Files
PulsarK-main/Pulsar.Server/Controls/Wpf/HeatMapViewModel.cs
T

232 lines
6.7 KiB
C#
Raw Normal View History

2026-08-27 10:57:58 -06:00
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using LiveChartsCore.Geo;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using Pulsar.Server.Statistics;
#nullable enable
namespace Pulsar.Server.Controls.Wpf
{
public sealed class HeatMapViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<StatCardViewModel> _statCards = new()
{
new StatCardViewModel("Total Clients"),
new StatCardViewModel("Geolocated"),
new StatCardViewModel("Unknown Location"),
new StatCardViewModel("Unique Countries")
};
private readonly ObservableCollection<CountryHeatItem> _topCountries = new();
private HeatLandSeries[] _series = Array.Empty<HeatLandSeries>();
private bool _isLoading;
private bool _hasError;
private string? _errorMessage;
private string _lastUpdated = string.Empty;
private bool _isDarkMode;
private ClientGeoSnapshot? _lastSnapshot;
public event PropertyChangedEventHandler? PropertyChanged;
public ReadOnlyObservableCollection<StatCardViewModel> StatCards { get; }
public HeatLandSeries[] Series
{
get => _series;
private set => SetField(ref _series, value);
}
public bool IsLoading
{
get => _isLoading;
private set
{
if (SetField(ref _isLoading, value))
{
OnPropertyChanged(nameof(IsContentVisible));
}
}
}
public bool HasError
{
get => _hasError;
private set
{
if (SetField(ref _hasError, value))
{
OnPropertyChanged(nameof(IsContentVisible));
}
}
}
public bool IsContentVisible => !IsLoading && !HasError;
public string? ErrorMessage
{
get => _errorMessage;
private set => SetField(ref _errorMessage, value);
}
public string LastUpdated
{
get => _lastUpdated;
private set => SetField(ref _lastUpdated, value);
}
public ReadOnlyObservableCollection<CountryHeatItem> TopCountries { get; }
public HeatMapViewModel()
{
StatCards = new ReadOnlyObservableCollection<StatCardViewModel>(_statCards);
TopCountries = new ReadOnlyObservableCollection<CountryHeatItem>(_topCountries);
}
public void SetLoading()
{
ErrorMessage = null;
HasError = false;
IsLoading = true;
}
public void SetError(string message)
{
_lastSnapshot = null;
ErrorMessage = message;
HasError = true;
IsLoading = false;
LastUpdated = string.Empty;
ClearData();
}
public void UpdateSnapshot(ClientGeoSnapshot snapshot)
{
_lastSnapshot = snapshot;
ErrorMessage = snapshot.ErrorMessage;
HasError = snapshot.HasError;
IsLoading = false;
if (snapshot.HasError)
{
LastUpdated = string.Empty;
ClearData();
return;
}
LastUpdated = $"Updated {snapshot.GeneratedAtUtc.ToLocalTime():g}";
UpdateCards(snapshot);
UpdateTopCountries(snapshot);
BuildSeries();
}
public void UpdateTheme(bool isDarkMode)
{
_isDarkMode = isDarkMode;
BuildSeries();
}
private void UpdateCards(ClientGeoSnapshot snapshot)
{
_statCards[0].Update(snapshot.TotalClients.ToString("N0"), "Records processed");
_statCards[1].Update(snapshot.MappedClients.ToString("N0"), "Known country");
_statCards[2].Update(snapshot.UnknownClients.ToString("N0"), "No location data");
_statCards[3].Update(snapshot.UniqueCountryCount.ToString("N0"), "Countries represented");
}
private void UpdateTopCountries(ClientGeoSnapshot snapshot)
{
_topCountries.Clear();
var rank = 1;
foreach (var country in snapshot.Countries.Take(15))
{
_topCountries.Add(new CountryHeatItem(rank++, country.Name, country.CountryCode3.ToUpperInvariant(), country.Count, country.Share));
}
}
private void BuildSeries()
{
if (_lastSnapshot == null || _lastSnapshot.HasError)
{
Series = Array.Empty<HeatLandSeries>();
return;
}
var lands = _lastSnapshot.Countries
.Where(c => !string.IsNullOrWhiteSpace(c.CountryCode3))
.Select(c => new HeatLand
{
Name = c.CountryCode3.ToLowerInvariant(),
Value = c.Count
})
.ToArray();
if (lands.Length == 0)
{
Series = Array.Empty<HeatLandSeries>();
return;
}
Series = new[]
{
new HeatLandSeries
{
Lands = lands
}
};
}
private void ClearData()
{
Series = Array.Empty<HeatLandSeries>();
_topCountries.Clear();
}
private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed class CountryHeatItem
{
public CountryHeatItem(int rank, string country, string code, int count, double share)
{
Rank = rank;
Country = string.IsNullOrWhiteSpace(country) ? "Unknown" : country;
Code = string.IsNullOrWhiteSpace(code) ? "" : code;
Count = count;
Share = share;
}
public int Rank { get; }
public string Country { get; }
public string Code { get; }
public int Count { get; }
public double Share { get; }
public string SharePercent => Share > 0 ? Share.ToString("P1") : "0%";
}
}