@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. *@
@if (_pivotRows is { Length: > 0 } && (ShowIfEmpty || (!PivotData.Get(0, 0)?.ToString()?.Equals("No results") ?? false))) { @if (!string.IsNullOrEmpty(Title)) {

@Title

} @foreach (var field in _pivotRows[0].GetDynamicMemberNames()) { var headerStyle = HeaderStyles.GetValueOrDefault(field, ""); } }
@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 GetColumnDescriptor { get; set; } = _ => null; [Parameter] public Func GetFieldDescriptor { get; set; } = _ => null; [Parameter] public Func> HandleCellClick { get; set; } = (_, _) => Task.FromResult(false); [Parameter] public string Title { get; set; } = ""; [Parameter] public bool ShowIfEmpty { get; set; } = true; [Parameter] public Dictionary HeaderStyles { get; set; } = []; [Parameter] public bool IsExportEnabled { get => _pivotRows is { Length: > 0 }; // ReSharper disable once ValueParameterNotUsed set { // ignore } } [Parameter] public EventCallback 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 ? _fieldTypes; private List _filteredRows = []; private readonly DelayedExecution _delayedUpdate = new(TimeSpan.FromMilliseconds(1500)); private Dictionary _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 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 _descriptorsCache = []; private readonly Dictionary _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 FilteredRows { get => _filteredRows; set { _filteredRows = value; _totals.Update(PivotData, _filteredRows.OfType().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().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() ) { 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; } }