mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-28 23:54:59 +00:00
feature<Statistics>: add simple statistic page
This commit is contained in:
parent
f04c01b878
commit
c52ed4a711
10 changed files with 512 additions and 0 deletions
118
src/Views/Controls/Chart.cs
Normal file
118
src/Views/Controls/Chart.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.Views.Controls {
|
||||
/// <summary>
|
||||
/// 绘制提交频率柱状图
|
||||
/// </summary>
|
||||
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<Models.StatisticSample> samples = new List<Models.StatisticSample>();
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制数据
|
||||
/// </summary>
|
||||
/// <param name="samples">数据源</param>
|
||||
public void SetData(List<Models.StatisticSample> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
src/Views/Statistics.xaml
Normal file
195
src/Views/Statistics.xaml
Normal file
|
@ -0,0 +1,195 @@
|
|||
<controls:Window
|
||||
x:Class="SourceGit.Views.Statistics"
|
||||
x:Name="me"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:SourceGit.Views.Controls"
|
||||
mc:Ignorable="d"
|
||||
Title="Statistics"
|
||||
Height="450" Width="600"
|
||||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="1"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title Bar -->
|
||||
<Grid Grid.Row="0" Background="{DynamicResource Brush.TitleBar}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon -->
|
||||
<Path Grid.Column="0" Margin="6,0" Width="16" Height="16" Data="{StaticResource Icon.Statistics}"/>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Column="1" Text="{DynamicResource Text.Statistics}"/>
|
||||
|
||||
<!-- Window Commands -->
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<controls:IconButton Click="Quit" Width="28" Padding="9" Icon="{StaticResource Icon.Close}" HoverBackground="Red" Opacity="1"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{DynamicResource Brush.Border0}"/>
|
||||
|
||||
<!-- Contents -->
|
||||
<TabControl
|
||||
Grid.Row="2"
|
||||
Margin="8"
|
||||
Style="{DynamicResource Style.TabControl.MiddleSwitch}">
|
||||
<TabItem Header="{DynamicResource Text.Statistics.ThisWeek}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="32"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<DataGrid
|
||||
x:Name="lstCommitterWeek"
|
||||
Grid.Row="0" Grid.Column="0"
|
||||
Margin="0,8,0,0"
|
||||
Background="{DynamicResource Brush.Contents}"
|
||||
GridLinesVisibility="All"
|
||||
HorizontalGridLinesBrush="{DynamicResource Brush.Border0}"
|
||||
VerticalGridLinesBrush="{DynamicResource Brush.Border0}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
HeadersVisibility="Column"
|
||||
RowHeight="24"
|
||||
ColumnHeaderHeight="24"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CanUserReorderColumns="False"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource Brush.Border0}">
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="{x:Type DataGridColumnHeader}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
|
||||
<Border BorderThickness="0,0,1,1" BorderBrush="{DynamicResource Brush.Border0}" Background="{DynamicResource Brush.Window}">
|
||||
<TextBlock
|
||||
Text="{TemplateBinding Content}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="DemiBold"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Width="*" Header="{DynamicResource Text.Statistics.CommitterName}" IsReadOnly="True" Binding="{Binding .Name}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
|
||||
<DataGridTextColumn Width="*" Header="{DynamicResource Text.Statistics.CommitAmount}" IsReadOnly="True" Binding="{Binding .Count}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" x:Name="txtMemberCountWeek" Text="Total Committers: -"/>
|
||||
|
||||
<controls:Chart
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="8,16,0,0"
|
||||
x:Name="chartWeek"
|
||||
LineBrush="{DynamicResource Brush.FG1}"
|
||||
ChartBrush="{DynamicResource Brush.Accent1}"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" x:Name="txtCommitCountWeek" HorizontalAlignment="Right" Text="Total Commits: -"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="{DynamicResource Text.Statistics.ThisMonth}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="32"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<DataGrid
|
||||
x:Name="lstCommitterMonth"
|
||||
Grid.Row="0" Grid.Column="0"
|
||||
Margin="0,8,0,0"
|
||||
Background="{DynamicResource Brush.Contents}"
|
||||
GridLinesVisibility="All"
|
||||
HorizontalGridLinesBrush="{DynamicResource Brush.Border0}"
|
||||
VerticalGridLinesBrush="{DynamicResource Brush.Border0}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
HeadersVisibility="Column"
|
||||
RowHeight="24"
|
||||
ColumnHeaderHeight="24"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CanUserReorderColumns="False"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource Brush.Border0}">
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="{x:Type DataGridColumnHeader}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
|
||||
<Border BorderThickness="0,0,1,1" BorderBrush="{DynamicResource Brush.Border0}" Background="{DynamicResource Brush.Window}">
|
||||
<TextBlock
|
||||
Text="{TemplateBinding Content}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="DemiBold"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Width="*" Header="{DynamicResource Text.Statistics.CommitterName}" IsReadOnly="True" Binding="{Binding .Name}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
|
||||
<DataGridTextColumn Width="*" Header="{DynamicResource Text.Statistics.CommitAmount}" IsReadOnly="True" Binding="{Binding .Count}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" x:Name="txtMemberCountMonth" Text="Total Committers: -"/>
|
||||
|
||||
<controls:Chart
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="8,16,0,0"
|
||||
x:Name="chartMonth"
|
||||
LineBrush="{DynamicResource Brush.FG1}"
|
||||
ChartBrush="{DynamicResource Brush.Accent1}"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" x:Name="txtCommitCountMonth" HorizontalAlignment="Right" Text="Total Commits: -"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- Loading -->
|
||||
<controls:Loading Grid.Row="2" x:Name="loading" Width="48" Height="48" IsAnimating="True"/>
|
||||
</Grid>
|
||||
</controls:Window>
|
120
src/Views/Statistics.xaml.cs
Normal file
120
src/Views/Statistics.xaml.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SourceGit.Views {
|
||||
/// <summary>
|
||||
/// 提交统计
|
||||
/// </summary>
|
||||
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<int, Models.StatisticSample>();
|
||||
for (int i = 0; i < 7; i++) {
|
||||
mapsWeek.Add(i, new Models.StatisticSample {
|
||||
Name = $"星期{WEEK_DAYS[i]}",
|
||||
Count = 0,
|
||||
});
|
||||
}
|
||||
|
||||
var mapsMonth = new Dictionary<int, Models.StatisticSample>();
|
||||
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<string, Models.StatisticSample>();
|
||||
var mapCommitterMonth = new Dictionary<string, Models.StatisticSample>();
|
||||
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<Models.StatisticSample>();
|
||||
var samplesChartMonth = new List<Models.StatisticSample>();
|
||||
var samplesCommittersWeek = new List<Models.StatisticSample>();
|
||||
var samplesCommittersMonth = new List<Models.StatisticSample>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -108,6 +108,13 @@
|
|||
IsChecked="{Binding Source={x:Static models:Preference.Instance}, Path=Window.MoveCommitInfoRight, Mode=TwoWay, Converter={StaticResource InverseBool}}"
|
||||
Checked="ChangeOrientation" Unchecked="ChangeOrientation"/>
|
||||
|
||||
<controls:IconButton
|
||||
Margin="8,0"
|
||||
Padding="0,8"
|
||||
Icon="{DynamicResource Icon.Statistics}"
|
||||
ToolTip="{DynamicResource Text.Dashboard.Statistics}"
|
||||
Click="OpenStatistics"/>
|
||||
|
||||
<controls:IconButton
|
||||
Margin="8,0"
|
||||
Padding="0,9,0,8"
|
||||
|
|
|
@ -408,6 +408,11 @@ namespace SourceGit.Views.Widgets {
|
|||
(pages.Get("histories") as Histories)?.ChangeOrientation();
|
||||
}
|
||||
|
||||
private void OpenStatistics(object sender, RoutedEventArgs e) {
|
||||
var dialog = new Statistics(repo.Path) { Owner = App.Current.MainWindow };
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void OpenConfigure(object sender, RoutedEventArgs e) {
|
||||
new Popups.Configure(repo.Path).Show();
|
||||
e.Handled = true;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue