How to fix Aggregate Measurements in order to display properly?
Hi,
I am working on a Blazor application for weight measurement. Data is coming from a third-party device in real time. I have this weighing results component on my dashboard that I am trying to display on a grid according to some rules. The data grid periodically loads data.
Aggregate Measurements will calculate the client's (Oğulbey, Yeşilköy) measurement and plate number (06GG111, 34HH222) on the current day. You can see the trend change from the 2-day sample data. It starts from 3000 for Oğulbey,06GG111and goes up to 4224 for 2024-11-27. (It is the first sequence) Then it goes down to 3000 for Oğulbey,06GG111 on the same day, and goes to 4068. (It is the second sequence) This is the definition of trend change. (Same as the decrease)
Here is a 2-day sample data in the database
3000 2024-11-26 16:45:49 Oğulbey 06GG111
3345 2024-11-26 16:45:56 Yeşilköy 34HH222
3426 2024-11-26 16:46:56 Oğulbey 06GG111
3771 2024-11-26 16:47:26 Yeşilköy 34HH222
3000 2024-11-27 16:59:27 Oğulbey 06GG111
3345 2024-11-27 16:59:48 Yeşilköy 34HH222
3661 2024-11-27 17:00:27 Oğulbey 06GG111
4006 2024-11-27 17:00:40 Yeşilköy 34HH222
4224 2024-11-27 17:01:27 Oğulbey 06GG111
3000 2024-11-27 17:04:11 Oğulbey 06GG111
3345 2024-11-27 17:04:30 Yeşilköy 34HH222
3530 2024-11-27 17:05:11 Oğulbey 06GG111
3875 2024-11-27 17:05:24 Yeşilköy 34HH222
4068 2024-11-27 17:06:11 Oğulbey 06GG111
expected output after Aggregate measurement:
(explanation not displayed: Second sequence because of trend change)
3000 4068 2024-11-27 17:06:11 Oğulbey 06GG111
3345 3875 2024-11-27 17:05:24 Yeşilköy 34HH222
(explanation not displayed: first sequence)
3000 4224 2024-11-27 17:01:27 Oğulbey 06GG111
3345 4006 2024-11-27 17:00:40 Yeşilköy 34HH222
(explanation not displayed: previous day)
3345 3771 2024-11-26 16:47:26 Yeşilköy 34HH222
3000 3426 2024-11-26 16:46:56 Oğulbey 06GG111
Here is the component but it does not give the expected output.
@page "/weighing-results"
@using OfficeOpenXml
@using OfficeOpenXml.Style
@using Radzen
@using Radzen.Blazor
@using WeighingDeviceDashboard.Service
@inject IMeasurementService MeasurementService
@inject IJSRuntime JS
<style>
.highlight {
animation: blink 1s alternate 5; /* Adjust duration, iterations, and timing as needed */
/* animation-fill-mode: forwards; */
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
@@keyframes blink {
0% {
background-color: #ff0000; /* Yellow (highlight color) */
font-weight: bold;
}
50% {
background-color: #ffffff; /* White (default background color) */
}
100% {
background-color: #ff0000; /* Yellow (highlight color) */
font-weight: bold;
}
}
</style>
<RadzenButton Text="Export" Icon="grid_on" Click="@(args => ExportExcel())" Class="mb-4 mr-2" ButtonStyle="ButtonStyle.Secondary" Size="ButtonSize.Small" />
<RadzenDataGrid @ref=grid Data="@weighingResults" TItem="WeighingResult" AllowFiltering="true" AllowColumnResize="false" AllowAlternatingRows="true"
FilterMode="FilterMode.Advanced" AllowSorting="true" PageSize="15" AllowPaging="true" AllowGrouping="true" PagerHorizontalAlign="HorizontalAlign.Left"
ShowPagingSummary="true" ColumnWidth="300px" LogicalFilterOperator="LogicalFilterOperator.And"
IsLoading=@isLoading Sort="@ShowLoading" Page="@ShowLoading" Group="@ShowLoading" Filter="@ShowLoading">
<Columns>
<RadzenDataGridColumn Property="@nameof(WeighingResult.ClientId)" Title="Warehouse" Width="150px" />
<RadzenDataGridColumn Property="@nameof(WeighingResult.PlateNumber)" Title="Plate Number" Width="150px">
<Template Context="result">
<span class="@((result as WeighingResult).IsNew? "highlight" : "")">@result.PlateNumber</span>
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn Property="@nameof(WeighingResult.FirstWeighingValue)" Title="First Weighing (KG)" Width="200px">
<Template Context="result">
<span>@result.FirstWeighingValue.ToString("F0")</span>
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn Property="@nameof(WeighingResult.LastWeighingValue)" Title="Last Weighing (KG)" Width="200px">
<Template Context="result">
<span>@result.LastWeighingValue.ToString("F0")</span>
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn Property="@nameof(WeighingResult.WeightDifference)" Title="Weight Difference (KG)" Width="230px">
<Template Context="result">
<span class="@((result as WeighingResult).IsNew? "highlight" : "")">
@(result.WeightDifference!= null?
(result.WeightDifference > 0? $"+{result.WeightDifference.Value.ToString("F0")}" :
(result.WeightDifference < 0? $"{result.WeightDifference.Value.ToString("F0")}" : $"0"))
: "-")
</span>
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn Property="@nameof(WeighingResult.LastWeighingDateTime)" Title="Last Weighing Date Time" Width="200px" />
<RadzenDataGridColumn Property="@nameof(WeighingResult.ReceiptNo)" Title="Receipt Number" Width="200px" />
</Columns>
</RadzenDataGrid>
@code {
RadzenDataGrid<WeighingResult> grid = new();
List<WeighingResult> weighingResults = new List<WeighingResult>();
private readonly SemaphoreSlim _measurementLock = new SemaphoreSlim(1, 1);
bool isLoading = false;
private CancellationTokenSource _cancellationTokenSource;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_cancellationTokenSource = new CancellationTokenSource();
await LoadDataPeriodically(_cancellationTokenSource.Token);
}
private async Task LoadDataPeriodically(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await ShowLoading();
weighingResults = await GetWeighingResultsAsync();
await grid.Reload();
StateHasChanged(); // Ensure UI updates
try
{
await Task.Delay(60000, cancellationToken); // 1,2 minutes delay
}
catch (TaskCanceledException)
{
// Task was canceled, exit the loop
break;
}
}
}
private async Task ShowLoading()
{
isLoading = true;
await Task.Yield();
isLoading = false;
}
private async Task<List<WeighingResult>> GetWeighingResultsAsync()
{
await _measurementLock.WaitAsync();
try
{
var measurements = await MeasurementService.GetMeasurementsAsync();
return AggregateMeasurements(measurements);
}
finally
{
_measurementLock.Release();
}
}
private List<WeighingResult> AggregateMeasurements(IEnumerable<Measurement> measurements)
{
var results = new List<WeighingResult>();
var current_date = DateTime.Now.Date;
var measurementGroups = measurements
.GroupBy(m => (m.ClientId, m.PlateNumber, m.Timestamp.Date))
.Select(g => new
{
Group = g.OrderBy(m => m.Timestamp).ToList(),
ClientId = g.Key.ClientId,
PlateNumber = g.Key.PlateNumber,
Date = g.Key.Date
});
foreach (var groupInfo in measurementGroups)
{
int sequence = 1;
Trend? currentTrend = null;
Measurement? previousMeasurement = null;
WeighingResult? currentResult = null;
foreach (var measurement in groupInfo.Group)
{
var currentValue = (float)measurement.Value;
// Determine trend
Trend? newTrend = null;
if (previousMeasurement != null)
{
var previousValue = (float)previousMeasurement.Value;
newTrend = GetTrend(previousValue, currentValue);
}
// Check for trend change
if (previousMeasurement != null && newTrend != currentTrend)
{
// Create a new result when trend changes
sequence++;
currentResult = new WeighingResult
{
ClientId = measurement.ClientId,
PlateNumber = measurement.PlateNumber,
FirstWeighingValue = currentValue,
LastWeighingValue = currentValue,
LastWeighingDateTime = measurement.Timestamp,
ReceiptNo = measurement.ReceiptNo,
WeighingSequence = sequence,
IsNew = true // Mark as new for highlighting
};
results.Add(currentResult);
}
else if (currentResult == null)
{
// Create the first result
currentResult = new WeighingResult
{
ClientId = measurement.ClientId,
PlateNumber = measurement.PlateNumber,
FirstWeighingValue = currentValue,
LastWeighingValue = currentValue,
LastWeighingDateTime = measurement.Timestamp,
ReceiptNo = measurement.ReceiptNo,
WeighingSequence = sequence,
IsNew = true // Mark as new for highlighting
};
results.Add(currentResult);
}
else
{
// Update existing result
if (currentValue < currentResult.FirstWeighingValue)
{
currentResult.FirstWeighingValue = currentValue;
}
if (currentValue > currentResult.LastWeighingValue)
{
currentResult.LastWeighingValue = currentValue;
currentResult.LastWeighingDateTime = measurement.Timestamp;
currentResult.ReceiptNo = measurement.ReceiptNo;
}
}
// Update trend tracking
currentTrend = newTrend;
previousMeasurement = measurement;
}
}
// Sort results by date and sequence
return results
.OrderByDescending(r => r.LastWeighingDateTime)
.ThenBy(r => r.WeighingSequence)
.ToList();
}
private Trend? GetTrend(float previousValue, float currentValue)
{
if (currentValue > previousValue) return Trend.Increasing;
if (currentValue < previousValue) return Trend.Decreasing;
return null;
}
private enum Trend
{
Increasing,
Decreasing
}
private async Task ExportExcel()
{
var filter = grid.View;
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
var stream = new MemoryStream();
using (var package = new ExcelPackage(stream))
{
var workSheet = package.Workbook.Worksheets.Add("Weighing Results");
var recordIndex = 2;
workSheet.Row(1).Style.Font.Bold = true;
var headerCells = workSheet.Cells["A1:G1"];
headerCells.Style.Font.Bold = true;
headerCells.Style.Font.Size = 13;
headerCells.Style.Border.BorderAround(ExcelBorderStyle.Thin);
headerCells.Style.Border.Top.Style = ExcelBorderStyle.Thin;
headerCells.Style.Border.Left.Style = ExcelBorderStyle.Thin;
headerCells.Style.Border.Right.Style = ExcelBorderStyle.Thin;
headerCells.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
headerCells.Style.Font.Color.SetColor(System.Drawing.Color.Black);
headerCells.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
headerCells.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
workSheet.Cells[1, 1].Value = "Warehouse";
workSheet.Cells[1, 2].Value = "Plate Number";
workSheet.Cells[1, 3].Value = "First Weighing Value";
workSheet.Cells[1, 4].Value = "Last Weighing Value";
workSheet.Cells[1, 5].Value = "Weight Difference";
workSheet.Cells[1, 6].Value = "Last Weighing Date Time";
workSheet.Cells[1, 7].Value = "Receipt Number";
foreach (var result in filter)
{
workSheet.Cells[recordIndex, 1].Value = result.ClientId;
workSheet.Cells[recordIndex, 2].Value = result.PlateNumber;
workSheet.Cells[recordIndex, 3].Value = result.FirstWeighingValue.ToString("F0");
workSheet.Cells[recordIndex, 4].Value = result.LastWeighingValue.ToString("F0");
workSheet.Cells[recordIndex, 5].Value = result.WeightDifference?.ToString("F0");
workSheet.Cells[recordIndex, 6].Value = result.LastWeighingDateTime.ToString("g");
workSheet.Cells[recordIndex, 7].Value = result.ReceiptNo;
recordIndex++;
}
workSheet.Cells[workSheet.Dimension.Address].AutoFitColumns();
await package.SaveAsync();
}
stream.Position = 0;
var excelName = $"Weighing Results-{DateTime.Now.ToString("ddMMyyyyHHmm")}.xlsx";
using var streamRef = new DotNetStreamReference(stream: stream);
await JS.InvokeVoidAsync("downloadFileFromStream", excelName, streamRef);
}
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
}
}