442 lines
15 KiB
Plaintext
442 lines
15 KiB
Plaintext
@using System.Drawing
|
|
@using System.Dynamic
|
|
@using ChartJs.Blazor
|
|
@using ChartJs.Blazor.LineChart
|
|
@using Rms.Risk.Mango.Pivot.Core
|
|
|
|
@inject IJSRuntime Js
|
|
@inject NavigationManager NavigationManager
|
|
@inject IPivotSharingService PivotSharingService
|
|
|
|
@*
|
|
* dbMango
|
|
*
|
|
* Copyright 2025 Deutsche Bank AG
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*@
|
|
|
|
<div class="@Class">
|
|
|
|
<style>
|
|
.chart-canvas {
|
|
width: 1024px;
|
|
height: 470px;
|
|
margin-bottom: 50px;
|
|
}
|
|
|
|
.glow-pos {
|
|
background: #279320;
|
|
}
|
|
|
|
.glow-neg {
|
|
background: #723e36;
|
|
}
|
|
|
|
.glow-last {
|
|
border-bottom-color: yellow;
|
|
border-bottom-style: double;
|
|
}
|
|
</style>
|
|
|
|
<div class="@ChartClass">
|
|
<div class="chart-canvas ml-3">
|
|
<Chart Config="@ChartConfig"/>
|
|
</div>
|
|
</div>
|
|
|
|
@if (_pivotRows is { Length: > 0 }
|
|
&& (ShowIfEmpty || (!PivotData.Get(0, 0)?.ToString()?.Equals("No results") ?? false)))
|
|
{
|
|
@if (!string.IsNullOrEmpty(Title))
|
|
{
|
|
<h2>@Title</h2>
|
|
}
|
|
<TableControl Class="table table-hover table-striped table-forge fit-content"
|
|
Items="@_pivotRows"
|
|
PageSize="@Rows"
|
|
@bind-FilteredItems="FilteredRows"
|
|
CellStyle="GetCellStyle"
|
|
GetCellClassCallback="GetCellClassCallback"
|
|
Totals="@VisibleTotals"
|
|
@bind-SortMode="SortMode"
|
|
@bind-CurrentSortColumn="SortColumn"
|
|
>
|
|
@foreach (var field in _pivotRows[0].GetDynamicMemberNames())
|
|
{
|
|
|
|
var headerStyle = HeaderStyles.GetValueOrDefault(field, "");
|
|
|
|
<TableColumnControl Name="@field" Field="@field" Format="@_pivotRows[0].GetFormat(field)" ShowTotals="@_pivotRows[0].ShouldShowTotals(field)" Class="@ColumnClass" HeaderStyle="@headerStyle">
|
|
<Template>
|
|
<span @onclick="@(() => OnCellClick((DynamicObject)context.Row, field))">
|
|
@TableControl.ConvertToString(TableControl.GetLambdaForValue(field)(context.Row), _pivotRows[0].GetFormat(field))
|
|
</span>
|
|
</Template>
|
|
</TableColumnControl>
|
|
}
|
|
</TableControl>
|
|
}
|
|
</div>
|
|
|
|
@code{
|
|
[CascadingParameter] public IModalService Modal { get; set; } = null!;
|
|
|
|
[Parameter] public string Class { get; set; } = "";
|
|
|
|
[Parameter, EditorRequired]
|
|
public IPivotedData PivotData
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
if (field == value || value == null)
|
|
return;
|
|
|
|
field = value;
|
|
_totals.Clear();
|
|
|
|
_shadowHeaders = field.GetColumnPositions();
|
|
|
|
_descriptorsCache.Clear();
|
|
_fieldDescriptorsCache.Clear();
|
|
|
|
_pivotRows = Enumerable
|
|
.Range(0, field.Count)
|
|
.Select(x => new PivotRow(field, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal))
|
|
.ToArray()
|
|
;
|
|
|
|
_totals.Update(field);
|
|
|
|
if (PivotDef != null)
|
|
{
|
|
try
|
|
{
|
|
ChartHelper.UpdateLineChart(PivotDef, field, x => _pivotRows[0].GetFormat(x));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
} = new ArrayBasedPivotData([]);
|
|
|
|
[Parameter] public PivotDefinition? PivotDef { get; set; }
|
|
[Parameter] public GroupedCollection? SelectedCollectionNode { get; set; }
|
|
[Parameter] public int Rows { get; set; } = 35;
|
|
[Parameter] public Func<string /*columnName*/, PivotColumnDescriptor?> GetColumnDescriptor { get; set; } = _ => null;
|
|
[Parameter] public Func<string /*columnName*/, PivotFieldDescriptor?> GetFieldDescriptor { get; set; } = _ => null;
|
|
[Parameter] public Func<DynamicObject, string, Task<bool>> HandleCellClick { get; set; } = (_, _) => Task.FromResult(false);
|
|
[Parameter] public string Title { get; set; } = "";
|
|
[Parameter] public bool ShowIfEmpty { get; set; } = true;
|
|
[Parameter] public Dictionary<string, string> HeaderStyles { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public bool IsExportEnabled
|
|
{
|
|
get => _pivotRows is { Length: > 0 };
|
|
// ReSharper disable once ValueParameterNotUsed
|
|
set
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
[Parameter] public EventCallback<bool> IsExportEnabledChanged { get; set; }
|
|
|
|
|
|
public Task CopyCsv() => CopyCsvInternal();
|
|
public Task ExportCsv(string destFileName) => ExportCsvInternal(destFileName);
|
|
|
|
private PivotRow[] _pivotRows = [];
|
|
private readonly MinMaxCache _totals = new();
|
|
private PivotColumnDescriptor[] ? _descriptors;
|
|
private Dictionary<string, PivotFieldDescriptor> ? _fieldTypes;
|
|
private List<dynamic> _filteredRows = [];
|
|
private readonly DelayedExecution _delayedUpdate = new(TimeSpan.FromMilliseconds(1500));
|
|
private Dictionary<string, int> _shadowHeaders = [];
|
|
|
|
private bool ShowTotals => PivotDef?.ShowTotals ?? false;
|
|
private IMinMaxCache? VisibleTotals => ShowTotals ? _totals : null;
|
|
private LineConfig ChartConfig => ChartHelper.ChartConfig;
|
|
private ChartHelperForPivot ChartHelper { get; } = new();
|
|
|
|
private string ChartClass => PivotDef == null || !ChartHelper.IsLineChart(PivotDef, PivotData)
|
|
? "d-none"
|
|
: ""
|
|
;
|
|
|
|
private string ColumnClass
|
|
{
|
|
get
|
|
{
|
|
//This is a little awkward, usually the table control will auto wrap long headers when they contain spaces, but we have some pivots currently that have
|
|
//a combination of long columns with and without spaces. Wrapping looks weird here, so we want to turn off wrapping if we can
|
|
//detect any column which has a long name without spaces
|
|
|
|
if ((_pivotRows[0].GetDynamicMemberNames().Count() <= 16
|
|
|| _pivotRows[0].GetDynamicMemberNames().Any(n => n.Length >= 10 && !n.Contains(" "))
|
|
)
|
|
)
|
|
return "table-nowrap";
|
|
else
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> OnCellClick(DynamicObject row, string fieldName)
|
|
{
|
|
try
|
|
{
|
|
await HandleCellClick(row, fieldName);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error drilling down into {fieldName}", e);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private readonly Dictionary<string, PivotColumnDescriptor> _descriptorsCache = [];
|
|
private readonly Dictionary<string, PivotFieldDescriptor?> _fieldDescriptorsCache = [];
|
|
|
|
private readonly PivotColumnDescriptor _defaultPivotColumnDescriptor = new()
|
|
{
|
|
Format = "N0",
|
|
NameRegexString = ".*",
|
|
Background = Color.FromName(Night.Background),
|
|
AlternateBackground = Color.FromName(Night.BackgroundLight)
|
|
};
|
|
|
|
private TableControl.SortModeType SortMode { get; set; }
|
|
private string? SortColumn { get; set; }
|
|
|
|
private PivotColumnDescriptor GetColumnDescriptorInternal(string columnName)
|
|
{
|
|
if (_descriptorsCache.TryGetValue(columnName, out var desc))
|
|
return desc;
|
|
|
|
_descriptors ??= SelectedCollectionNode?.ColumnDescriptors;
|
|
|
|
desc = GetColumnDescriptor(columnName)
|
|
?? _descriptors?.FirstOrDefault(x => x.NameRegex.IsMatch(columnName))
|
|
?? _defaultPivotColumnDescriptor;
|
|
|
|
_descriptorsCache[columnName] = desc;
|
|
return desc;
|
|
}
|
|
|
|
private PivotFieldDescriptor? GetFieldDescriptorInternal(string columnName)
|
|
{
|
|
if (_fieldDescriptorsCache.TryGetValue(columnName, out var desc))
|
|
return desc;
|
|
|
|
_fieldTypes ??= SelectedCollectionNode?.FieldTypes;
|
|
|
|
desc = GetFieldDescriptor(columnName);
|
|
if (desc == null)
|
|
_fieldTypes?.TryGetValue(columnName, out desc);
|
|
|
|
_fieldDescriptorsCache[columnName] = desc;
|
|
return desc;
|
|
}
|
|
|
|
private async Task CopyCsvInternal()
|
|
{
|
|
if (!IsExportEnabled)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var csv = _pivotRows[0].PivotData.CopyToCsv();
|
|
await Js.InvokeVoidAsync("DashboardUtils.CopyToClipboard", csv);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
await ModalDialogUtils.ShowExceptionDialog(Modal, "Error copying pivot data", e);
|
|
}
|
|
}
|
|
|
|
private async Task ExportCsvInternal(string destFileName)
|
|
{
|
|
if (!IsExportEnabled)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await using var writer = new StringWriter();
|
|
_pivotRows[0].PivotData.WriteToCsv(writer);
|
|
var data = writer.ToString();
|
|
|
|
var url = await PivotSharingService.ExportToCsv(
|
|
destFileName,
|
|
data
|
|
);
|
|
|
|
@* var url = await DownloadController.GetDownloadLink( *@
|
|
@* _storage, *@
|
|
@* _passwordManager, *@
|
|
@* _singleUseTokenService, *@
|
|
@* fileName => *@
|
|
@* { *@
|
|
@* _pivotRows![0].PivotData.WriteToCsv(fileName); *@
|
|
@* return Task.CompletedTask; *@
|
|
@* }, *@
|
|
@* destFileName *@
|
|
@* ); *@
|
|
|
|
await Js.InvokeVoidAsync("open", $"{NavigationManager.BaseUri}{url}", "_blank");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
await ModalDialogUtils.ShowExceptionDialog(Modal, "Error exporting pivot data", e);
|
|
}
|
|
}
|
|
|
|
// ReSharper disable once UnusedMember.Local
|
|
private List<dynamic> FilteredRows
|
|
{
|
|
get => _filteredRows;
|
|
set
|
|
{
|
|
_filteredRows = value;
|
|
_totals.Update(PivotData, _filteredRows.OfType<PivotRow>().Select( x => x.Row).ToArray());
|
|
|
|
if (_filteredRows.Count == 0 || PivotDef is not { MakeLineChart: true })
|
|
return;
|
|
|
|
_delayedUpdate.Run(UpdateFilteredChart, "UpdateFilteredChart");
|
|
}
|
|
}
|
|
|
|
private Task UpdateFilteredChart(CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
|
|
if (PivotDef == null || token.IsCancellationRequested)
|
|
return Task.CompletedTask;
|
|
|
|
var filteredPivot = new PivotFilteredView(_filteredRows.Cast<PivotRow>().ToList());
|
|
|
|
if (token.IsCancellationRequested)
|
|
return Task.CompletedTask;
|
|
|
|
ChartHelper.UpdateLineChart(PivotDef, filteredPivot, x => _pivotRows[0].GetFormat(x));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// ignore
|
|
}
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
public static string GetCellStyle(dynamic row, TableColumnControl col)
|
|
{
|
|
if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) )
|
|
return "";
|
|
|
|
var desc = r.GetColumnDescriptor(col.Field);
|
|
if ( desc == null )
|
|
return "";
|
|
|
|
// for some reason comparison with 0x00000 and Color.Black are not working
|
|
if ( desc.Background is { R: 0, G: 0, B: 0 })
|
|
return "";
|
|
|
|
return $"background-color:#{desc.Background.A:x2}{desc.Background.R:x2}{desc.Background.G:x2}{desc.Background.B:x2}";
|
|
}
|
|
|
|
private string GetCellClassCallback(dynamic row, TableColumnControl col)
|
|
{
|
|
var cellClass = "";
|
|
|
|
// make a copy as GetFieldDescriptor may take noticeable time
|
|
var highlightTopPercent = PivotDef?.HighlightTopPercent ?? 0.0;
|
|
|
|
if (highlightTopPercent <= 0.0)
|
|
return cellClass;
|
|
|
|
if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) )
|
|
return cellClass;
|
|
|
|
var mm = _totals.TryGet(col.Name);
|
|
if ( mm == null )
|
|
return cellClass;
|
|
|
|
var desc = r.GetFieldDescriptor(col.Field);
|
|
if ( desc != null && desc.Purpose != PivotFieldPurpose.Data )
|
|
return cellClass;
|
|
|
|
double val = GetCellValue(row, col.Field);
|
|
|
|
cellClass = val switch
|
|
{
|
|
> 0 when val > mm.MaxValue - mm.MaxValue * highlightTopPercent / 100.0 =>
|
|
$"{cellClass} glow-pos",
|
|
< 0 when val < mm.MinValue - mm.MinValue * highlightTopPercent / 100.0 =>
|
|
$"{cellClass} glow-neg",
|
|
_ => cellClass
|
|
};
|
|
|
|
// NaN here most likely means string column
|
|
if ( !double.IsNaN(val) && IsLastImportant(r, col.Name, mm) )
|
|
cellClass += " glow-last";
|
|
|
|
return cellClass;
|
|
}
|
|
|
|
private double GetCellValue(PivotRow row, string col)
|
|
{
|
|
if ( !_shadowHeaders.TryGetValue(col, out var idx) )
|
|
return double.NaN;
|
|
|
|
var val = row.PivotData.Get(idx, row.Row);
|
|
return val switch
|
|
{
|
|
double d => d,
|
|
int i => i,
|
|
_ => double.NaN
|
|
};
|
|
}
|
|
|
|
private bool IsLastImportant(PivotRow row, string colName, MinMax mm)
|
|
{
|
|
if ( SortColumn != colName || SortMode != TableControl.SortModeType.DescendingAbsolute)
|
|
return false;
|
|
|
|
var sum = 0.0;
|
|
var target = mm.AbsTotal * ( PivotDef?.HighlightTopPercent ?? 0.0 ) / 100.0;
|
|
|
|
foreach ( var r in FilteredRows.OfType<PivotRow>() )
|
|
{
|
|
var val = GetCellValue(r, colName);
|
|
if ( double.IsNaN(val) )
|
|
return false;
|
|
|
|
sum += Math.Abs(val);
|
|
|
|
if ( sum >= target )
|
|
return row.Row == r.Row;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|