Modifiche stacked bar chart

This commit is contained in:
Samuele Locatelli
2025-12-18 15:03:57 +01:00
parent b3f2afbe0b
commit 072a5de083
11 changed files with 293 additions and 178 deletions
+57
View File
@@ -0,0 +1,57 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace EgwCoreLib.Lux.Core
{
/// <summary>
/// Helper gestione colori RGB(A)
/// </summary>
public static class ColorHelper
{
#region Public Methods
/// <summary>
/// Imposta un valore di alpha specifico. Se la stringa non è valida, ritorna un fallback nero.
/// </summary>
/// <param name="color">Stringa tipo "rgb(0,0,0)" o "rgba(0,0,0,0.5)"</param>
/// <param name="alpha">Valore da 0.0 a 1.0</param>
public static string SetAlpha(string color, double alpha)
{
if (string.IsNullOrWhiteSpace(color)) return $"rgba(0,0,0,{alpha.ToString(CultureInfo.InvariantCulture)})";
// Cerchiamo i componenti R, G, B
var match = RgbComponentsRegex.Match(color);
if (match.Success)
{
// Estraiamo i primi 3 gruppi (R, G, B)
string r = match.Groups[1].Value;
string g = match.Groups[2].Value;
string b = match.Groups[3].Value;
// Usiamo InvariantCulture per assicurarci che il punto decimale sia sempre '.' e non ','
string alphaStr = alpha.ToString("0.0", CultureInfo.InvariantCulture);
return $"rgba({r},{g},{b},{alphaStr})";
}
// Se la stringa non è in formato rgb/rgba (magari è un nome colore o hex),
// per ora restituiamo l'originale o potresti gestire l'errore.
return color;
}
/// <summary>
/// Forza l'alpha a 1, indipendentemente dal formato originale (rgb o rgba).
/// </summary>
public static string ToOpaqueRgba(string color) => SetAlpha(color, 1.0);
#endregion Public Methods
#region Private Fields
// Pattern per catturare i tre componenti R, G, B ignorando l'eventuale A esistente
private static readonly Regex RgbComponentsRegex = new Regex(@"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", RegexOptions.Compiled);
#endregion Private Fields
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.9.2512.1812</Version>
<Version>0.9.2512.1815</Version>
</PropertyGroup>
<ItemGroup>
+1 -113
View File
@@ -1,113 +1 @@
@inject IJSRuntime JSRuntime
@implements IDisposable
@code {
[Parameter]
public Dictionary<string, double> PieDict { get; set; } = new();
[Parameter]
public Dictionary<string, string> PieColor { get; set; } = new();
[Parameter]
public string CanvasId { get; set; } = "pieChart";
[Parameter]
public string Label { get; set; } = "#";
private bool created = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// if (firstRender && PieDict.Any())
if (firstRender || !created)
{
created = true;
pieLabels = PieDict.Keys.ToList();
pieValues = PieDict.Values.ToList();
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
{
type = "pie",
data = new
{
labels = pieLabels,
datasets = new[]
{
new {
label = Label,
data = pieValues,
backgroundColor= bgColors,
borderColor= bordColors,
borderWidth= 2
}
}
},
options = new
{
responsive = true,
animation = false
}
});
}
}
public void Dispose()
{
// Chiama il metodo JS per pulire il dizionario
_ = JSRuntime.InvokeVoidAsync("chartsInterop.destroyChart", CanvasId);
}
private List<string> pieLabels = new();
private List<double> pieValues = new();
private List<string> bgColors = new();
private List<string> bordColors = new();
protected override async Task OnParametersSetAsync()
{
if (created)
{
pieLabels = PieDict.Keys.ToList();
pieValues = PieDict.Values.ToList();
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
// await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, pieLabels, values, bgColors);
// await JSRuntime.InvokeVoidAsync("chartsInterop.createChart", CanvasId, new
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
{
type = "pie",
data = new
{
labels = pieLabels,
datasets = new[]
{
new {
label = Label,
data = pieValues,
backgroundColor= bgColors,
borderColor= bordColors,
borderWidth= 2
}
}
},
options = new
{
responsive = true,
animation = false
}
});
}
else
{
pieLabels = PieDict.Keys.ToList();
pieValues = PieDict.Values.ToList();
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
}
}
}
<canvas id="@CanvasId"></canvas>
<canvas id="@CanvasId"></canvas>
@@ -0,0 +1,138 @@
using EgwCoreLib.Lux.Core;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Lux.UI.Components.Compo.Charts
{
public partial class PieChart : IDisposable
{
#region Public Properties
[Parameter]
public bool Animate { get; set; } = false;
[Parameter]
public string CanvasId { get; set; } = "pieChart";
[Parameter]
public string Label { get; set; } = "#";
/// <summary>
/// Lista colori in formato RGBA
/// es: rgba(0,0,0,0.3)
/// </summary>
[Parameter]
public Dictionary<string, string> PieColor { get; set; } = new();
[Parameter]
public Dictionary<string, double> PieDict { get; set; } = new();
[Parameter]
public bool Responsive { get; set; } = true;
[Parameter]
public double Alpha { get; set; } = 0.5;
#endregion Public Properties
#region Public Methods
public void Dispose()
{
// Chiama il metodo JS per pulire il dizionario
_ = JSRuntime.InvokeVoidAsync("chartsInterop.destroyChart", CanvasId);
}
#endregion Public Methods
#region Protected Properties
[Inject]
protected IJSRuntime JSRuntime { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
/// <summary>
/// Setup dati per plotting
/// </summary>
/// <returns></returns>
protected override async Task OnParametersSetAsync()
{
// solo se ho tutti i dati...
if (PieDict != null && PieDict.Count > 0)
{
await DoRenderChart();
}
}
#endregion Protected Methods
#region Private Fields
private List<string> bgColors = new();
private List<string> bordColors = new();
private List<string> pieLabels = new();
private List<double> pieValues = new();
#endregion Private Fields
#region Private Methods
/// <summary>
/// Conversione/trasformazione dati + chiamata js x rendering
/// </summary>
/// <returns></returns>
private async Task DoRenderChart()
{
pieLabels = PieDict.Keys.ToList();
pieValues = PieDict.Values.ToList();
#if false
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
#endif
// Sfondo: usiamo SetAlpha per garantire che, qualunque sia il colore nel dizionario, l'alpha sia Alpha
bgColors = pieLabels
.Select(l => {
string baseColor = PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0)";
return ColorHelper.SetAlpha(baseColor, Alpha);
})
.ToList();
// Bordo: partiamo dai bgColors e forziamo l'opacità totale
bordColors = bgColors
.Select(c => ColorHelper.ToOpaqueRgba(c))
.ToList();
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
{
type = "pie",
data = new
{
labels = pieLabels,
datasets = new[]
{
new {
label = Label,
data = pieValues,
backgroundColor= bgColors,
borderColor= bordColors,
borderWidth= 2
}
}
},
options = new
{
responsive = Responsive,
animation = Animate
}
});
}
#endregion Private Methods
}
}
@@ -1,5 +1,6 @@
@inject IJSRuntime JSRuntime
<canvas id="@CanvasId"></canvas>
@code {
[Parameter] public Dictionary<string, string> BarColor { get; set; } = new();
[Parameter] public List<string> ListX { get; set; } = new(); // puoi passare DateTime.ToString()
@@ -8,20 +9,45 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && DictY.Any())
// if (firstRender && DictY.Any())
if (DictY.Any())
{
var datasets = DictY.Select(kvp => new
{
label = kvp.Key,
data = kvp.Value,
backgroundColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,0.3)",
borderColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,1)",
// Usiamo il tuo nuovo ColorHelper!
backgroundColor = ColorHelper.SetAlpha(BarColor.GetValueOrDefault(kvp.Key, "rgba(0,0,0,0.3)"), 0.3),
borderColor = ColorHelper.ToOpaqueRgba(BarColor.GetValueOrDefault(kvp.Key, "rgba(0,0,0,1)")),
// backgroundColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,0.3)",
// borderColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,1)",
borderWidth = 2
}).ToList();
#if false
await JSRuntime.InvokeVoidAsync("chartsInterop.createBarChart", CanvasId, ListX, datasets);
#endif
// Passiamo l'intera configurazione per gestire lo 'stacked'
await JSRuntime.InvokeVoidAsync("chartsInterop.createBarChart", CanvasId, new
{
type = "bar",
data = new
{
labels = ListX,
datasets = datasets
},
options = new
{
responsive = true,
scales = new
{
x = new { stacked = true }, // Abilita lo stack su X
y = new { stacked = true } // Abilita lo stack su Y
}
}
});
}
}
}
<canvas id="@CanvasId"></canvas>
@@ -9,20 +9,20 @@
<div class="card-body">
<div class="row">
<div class="col-2">
<PieChart CanvasId="pieEventCount" PieDict="@PieCount" PieColor="@PieColors" Label="# Richieste" />
<PieChart CanvasId="pieEventCount" PieDict="@DatasetCount" PieColor="@PieColors" Label="# Richieste" />
</div>
<div class="col-10">
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@SetColors" /> *@
<StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@PieColors" />
</div>
</div>
<div class="row">
<div class="col-2">
<PieChart CanvasId="pieElapsed" PieDict="@PieElapsed" PieColor="@PieColors" Label="Elapsed"/>
<PieChart CanvasId="pieElapsed" PieDict="@DataseElapsed" PieColor="@PieColors" Label="Elapsed"/>
</div>
<div class="col-10">
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@SetColors" /> *@
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@PieColors" /> *@
</div>
</div>
<div class="row">
@@ -37,9 +37,6 @@ namespace Lux.UI.Components.Compo.Stats
{
if (firstRender)
{
#if false
InitConfig();
#endif
await InitCharts();
_cts = new CancellationTokenSource();
_ = UpdateLoop(_cts.Token);
@@ -140,37 +137,17 @@ namespace Lux.UI.Components.Compo.Stats
{
PieColors.Clear();
PieColors.Add("WINDOW", "rgba(54,162,235,0.6)");
PieColors.Add("BEAM", "rgba(255,99,132,0.6)");
#if false
Labels.Clear();
Labels.Add("WINDOW");
Labels.Add("BEAM");
PieCount.Clear();
PieCount.Add("WINDOW", 208);
PieCount.Add("BEAM", 13);
ListX.Clear();
ListX = new List<string> { "2025-12-17T00:00:00" };
DictY.Clear();
DictY.Add("WINDOW", new List<decimal> { 208, 150 });
DictY.Add("BEAM", new List<decimal> { 13, 20 });
#endif
PieCount.Clear();
PieCount.Add("NONE", 0);
PieElapsed.Clear();
PieElapsed.Add("NONE", 0);
PieColors.Add("BEAM", "rgba(69,195,132,0.6)");
DatasetCount.Clear();
DatasetCount.Add("NONE", 0);
DataseElapsed.Clear();
DataseElapsed.Add("NONE", 0);
}
private Dictionary<string, string> PieColors = new Dictionary<string, string>();
private Dictionary<string, double> PieCount = new Dictionary<string, double>();
private Dictionary<string, double> PieElapsed = new Dictionary<string, double>();
#if false
private List<string> Labels = new List<string>();
#endif
private Dictionary<string, double> DatasetCount = new Dictionary<string, double>();
private Dictionary<string, double> DataseElapsed = new Dictionary<string, double>();
private List<string> ListX = new List<string>();
private Dictionary<string, List<decimal>> DictY = new Dictionary<string, List<decimal>>();
@@ -220,27 +197,45 @@ namespace Lux.UI.Components.Compo.Stats
// calcolo i valori da mostrare...
if (rtProcStats != null && rtProcStats.Count > 0)
{
#if false
PieCount.Clear();
PieElapsed.Clear();
await InvokeAsync(StateHasChanged);
#endif
PieCount = rtProcStats
DatasetCount = rtProcStats
.GroupBy(p => p.TagClass)
.ToDictionary(
g => g.Key,
g => (double)g.Sum(p => p.EventCount)
);
PieElapsed = rtProcStats
DataseElapsed = rtProcStats
.GroupBy(p => p.TagClass)
.ToDictionary(
g => g.Key,
g => g.Sum(p => p.Elapsed)
);
// 1. Definiamo le etichette dell'asse X (es. le ultime 24 ore o quelle presenti nei dati)
ListX = rtProcStats
.Select(p => p.Hour.ToString("HH:mm"))
.Distinct()
.OrderBy(t => t)
.ToList();
// 2. Identifichiamo tutti i TagClass unici (saranno i nostri dataset)
var allTags = rtProcStats.Select(p => p.TagClass).Distinct().ToList();
// 3. Costruiamo il DictY: per ogni Tag, creiamo una lista di valori allineata a ListX
DictY = allTags.ToDictionary(
tag => tag,
tag => ListX.Select(hour =>
{
// Cerchiamo il valore per quel tag in quell'ora specifica
return (decimal)(rtProcStats
.FirstOrDefault(p => p.TagClass == tag && p.Hour.ToString("HH:mm") == hour)
?.EventCount ?? 0); // Se non esiste, mettiamo 0
}).ToList()
);
// forzatura update
await InvokeAsync(StateHasChanged);
}
}
+26 -15
View File
@@ -76,20 +76,31 @@
}
},
createBarChart: function (canvasId, labels, datasets) {
const ctx = document.getElementById(canvasId);
new Chart(ctx, {
type: "bar",
data: { labels: labels, datasets: datasets },
options: {
responsive: true,
plugins: { legend: { position: "top" } },
scales: {
x: { stacked: true, title: { display: true, text: "X Axis" } },
y: { stacked: true, title: { display: true, text: "Values" } }
}
}
});
createBarChart: function (canvasId, config) {
if (this.charts[canvasId]) {
const chart = this.charts[canvasId];
chart.data.labels = config.data.labels;
chart.data.datasets = config.data.datasets; // Sostituisce i dataset con i nuovi
chart.update('none'); // Update senza animazione per aggiornamenti real-time
} else {
const ctx = document.getElementById(canvasId).getContext('2d');
this.charts[canvasId] = new Chart(ctx, config);
}
}
//createBarChart: function (canvasId, labels, datasets) {
// const ctx = document.getElementById(canvasId);
// new Chart(ctx, {
// type: "bar",
// data: { labels: labels, datasets: datasets },
// options: {
// responsive: true,
// plugins: { legend: { position: "top" } },
// scales: {
// x: { stacked: true, title: { display: true, text: "X Axis" } },
// y: { stacked: true, title: { display: true, text: "Values" } }
// }
// }
// });
//}
};
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>LUX - Web Windows MES</i>
<h4>Versione: 0.9.2512.1812</h4>
<h4>Versione: 0.9.2512.1815</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
0.9.2512.1812
0.9.2512.1815
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>0.9.2512.1812</version>
<version>0.9.2512.1815</version>
<url>http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip</url>
<changelog>http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html</changelog>
<mandatory>false</mandatory>