feat(09-03): extend StorageViewModel with chart data properties and toggle
- Add IsDonutChart toggle, FileTypeMetrics collection, PieChartSeries, BarChartSeries, BarXAxes, BarYAxes - Add UpdateChartSeries method with top-10 + Other aggregation - Call CollectFileTypeMetricsAsync after storage scan in RunOperationAsync - Clear chart data on tenant switch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,15 @@ using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.SkiaSharpView;
|
||||
using LiveChartsCore.SkiaSharpView.Painting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Services;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace SharepointToolbox.ViewModels.Tabs;
|
||||
|
||||
@@ -33,6 +37,51 @@ public partial class StorageViewModel : FeatureViewModelBase
|
||||
[ObservableProperty]
|
||||
private int _folderDepth;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDonutChart = true;
|
||||
|
||||
private ObservableCollection<FileTypeMetric> _fileTypeMetrics = new();
|
||||
public ObservableCollection<FileTypeMetric> FileTypeMetrics
|
||||
{
|
||||
get => _fileTypeMetrics;
|
||||
private set
|
||||
{
|
||||
_fileTypeMetrics = value;
|
||||
OnPropertyChanged();
|
||||
UpdateChartSeries();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasChartData => FileTypeMetrics.Count > 0;
|
||||
|
||||
private IEnumerable<ISeries> _pieChartSeries = Enumerable.Empty<ISeries>();
|
||||
public IEnumerable<ISeries> PieChartSeries
|
||||
{
|
||||
get => _pieChartSeries;
|
||||
private set { _pieChartSeries = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private IEnumerable<ISeries> _barChartSeries = Enumerable.Empty<ISeries>();
|
||||
public IEnumerable<ISeries> BarChartSeries
|
||||
{
|
||||
get => _barChartSeries;
|
||||
private set { _barChartSeries = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private Axis[] _barXAxes = Array.Empty<Axis>();
|
||||
public Axis[] BarXAxes
|
||||
{
|
||||
get => _barXAxes;
|
||||
private set { _barXAxes = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private Axis[] _barYAxes = Array.Empty<Axis>();
|
||||
public Axis[] BarYAxes
|
||||
{
|
||||
get => _barYAxes;
|
||||
private set { _barYAxes = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsMaxDepth
|
||||
{
|
||||
get => FolderDepth >= 999;
|
||||
@@ -163,6 +212,22 @@ public partial class StorageViewModel : FeatureViewModelBase
|
||||
{
|
||||
Results = new ObservableCollection<StorageNode>(flat);
|
||||
}
|
||||
|
||||
// Collect file-type metrics for chart visualization
|
||||
progress.Report(OperationProgress.Indeterminate("Scanning file types for chart..."));
|
||||
var typeMetrics = await _storageService.CollectFileTypeMetricsAsync(ctx, progress, ct);
|
||||
|
||||
if (Application.Current?.Dispatcher is { } chartDispatcher)
|
||||
{
|
||||
await chartDispatcher.InvokeAsync(() =>
|
||||
{
|
||||
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(typeMetrics);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(typeMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTenantSwitched(TenantProfile profile)
|
||||
@@ -170,6 +235,7 @@ public partial class StorageViewModel : FeatureViewModelBase
|
||||
_currentProfile = profile;
|
||||
_hasLocalSiteOverride = false;
|
||||
Results = new ObservableCollection<StorageNode>();
|
||||
FileTypeMetrics = new ObservableCollection<FileTypeMetric>();
|
||||
SiteUrl = string.Empty;
|
||||
OnPropertyChanged(nameof(CurrentProfile));
|
||||
ExportCsvCommand.NotifyCanExecuteChanged();
|
||||
@@ -229,6 +295,88 @@ public partial class StorageViewModel : FeatureViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsDonutChartChanged(bool value)
|
||||
{
|
||||
UpdateChartSeries();
|
||||
}
|
||||
|
||||
private void UpdateChartSeries()
|
||||
{
|
||||
var metrics = FileTypeMetrics.ToList();
|
||||
OnPropertyChanged(nameof(HasChartData));
|
||||
|
||||
if (metrics.Count == 0)
|
||||
{
|
||||
PieChartSeries = Enumerable.Empty<ISeries>();
|
||||
BarChartSeries = Enumerable.Empty<ISeries>();
|
||||
BarXAxes = Array.Empty<Axis>();
|
||||
BarYAxes = Array.Empty<Axis>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Take top 10 by size, aggregate the rest as "Other"
|
||||
var top = metrics.Take(10).ToList();
|
||||
long otherSize = metrics.Skip(10).Sum(m => m.TotalSizeBytes);
|
||||
int otherCount = metrics.Skip(10).Sum(m => m.FileCount);
|
||||
|
||||
var chartItems = new List<FileTypeMetric>(top);
|
||||
if (otherSize > 0)
|
||||
chartItems.Add(new FileTypeMetric("Other", otherSize, otherCount));
|
||||
|
||||
// Pie/Donut series
|
||||
double innerRadius = IsDonutChart ? 50 : 0;
|
||||
PieChartSeries = chartItems.Select(m => new PieSeries<long>
|
||||
{
|
||||
Values = new[] { m.TotalSizeBytes },
|
||||
Name = m.DisplayLabel,
|
||||
InnerRadius = innerRadius,
|
||||
DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer,
|
||||
DataLabelsFormatter = point => m.DisplayLabel,
|
||||
ToolTipLabelFormatter = point =>
|
||||
$"{m.DisplayLabel}: {FormatBytes(m.TotalSizeBytes)} ({m.FileCount} files)"
|
||||
}).ToList();
|
||||
|
||||
// Bar chart series
|
||||
BarChartSeries = new ISeries[]
|
||||
{
|
||||
new ColumnSeries<long>
|
||||
{
|
||||
Values = chartItems.Select(m => m.TotalSizeBytes).ToArray(),
|
||||
Name = "Size",
|
||||
DataLabelsFormatter = point =>
|
||||
{
|
||||
int idx = (int)point.Index;
|
||||
return idx < chartItems.Count ? FormatBytes(chartItems[idx].TotalSizeBytes) : "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BarXAxes = new Axis[]
|
||||
{
|
||||
new Axis
|
||||
{
|
||||
Labels = chartItems.Select(m => m.DisplayLabel).ToArray(),
|
||||
LabelsRotation = -45
|
||||
}
|
||||
};
|
||||
|
||||
BarYAxes = new Axis[]
|
||||
{
|
||||
new Axis
|
||||
{
|
||||
Labeler = value => FormatBytes((long)value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
if (bytes < 1024) return $"{bytes} B";
|
||||
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
||||
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
|
||||
return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
|
||||
}
|
||||
|
||||
private static void FlattenNode(StorageNode node, int level, List<StorageNode> result)
|
||||
{
|
||||
node.IndentLevel = level;
|
||||
|
||||
Reference in New Issue
Block a user