@using System.Diagnostics @using System.Drawing @using System.Dynamic @using System.Reflection @using System.Text.RegularExpressions @using ChartJs.Blazor @using ChartJs.Blazor.LineChart @using log4net @using Rms.Risk.Mango.Pivot.Core @using Rms.Risk.Mango.Pivot.Core.Models @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 (_pivotRefreshing) {
Loading...
} else if (_pivotRows is { Length: > 0 } && (ShowIfEmpty || (!_pivotData?.Get(0, 0)?.ToString()?.Equals("No results") ?? false))) { @if (!string.IsNullOrEmpty(Title)) {

@Title

} @if (_pivotData?.Count == PivotMaxReturnedRows ) { } @foreach (var field in _pivotRows[0].GetDynamicMemberNames()) { var headerStyle = HeaderStyles.GetValueOrDefault(field, ""); } } @code{ private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!); [CascadingParameter] public IModalService Modal { get; set; } = null!; public static int PivotMaxReturnedRows = 500_000; [Parameter] public IPivotedData? PivotData { get => _pivotData; set { if (_pivotData == value) return; _pivotData = value; _totals.Clear(); if (_pivotData == null) { _pivotRows = []; InvokeAsync(StateHasChanged); return; } _shadowHeaders = _pivotData.GetColumnPositions(); _descriptorsCache.Clear(); _fieldDescriptorsCache.Clear(); _pivotRows = Enumerable .Range(0, _pivotData.Count ) .Select(x => new PivotRow(_pivotData, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal)) .ToArray() ; _totals.Update(_pivotData!); try { ChartHelper.UpdateLineChart(CurrentPivot!, _pivotData, x => _pivotRows[0].GetFormat(x)); } catch (Exception) { // ignore } PivotDataChanged.InvokeAsync(_pivotData); InvokeAsync(StateHasChanged); } } [Parameter] public EventCallback PivotDataChanged { get; set; } /// /// Unlike Pivot which contains pivot that would be executed CurrentPivot holds definition /// that is already shown and corresponding to PivotData. /// Always set PivotData and CurrentPivot at the same time. /// [Parameter] public PivotDefinition? CurrentPivot { get; set { if (value == null || field == value) return; field = value; CurrentPivotChanged.InvokeAsync(field); } } [Parameter] public EventCallback CurrentPivotChanged { get; set; } [Parameter] public GroupedCollection? SelectedCollectionNode { get; set; } [Parameter] public GroupedPivot? SelectedPivotNode { get => field; set { if (field == value) return; field = value; Filter = string.IsNullOrWhiteSpace(field?.Pivot.Filter) ? new() : FilterExpressionTree.ParseJson(field.Pivot.Filter) ; } } [Parameter] public int Rows { get; set; } = 35; [Parameter] public FilterExpressionTree.ExpressionGroup? ExtraFilter { get; set; } [Parameter] public bool IsExportEnabled { get => !(CurrentPivot == null || _pivotRows is not { Length: > 0 }); // ReSharper disable once ValueParameterNotUsed set { // ignore } } [Parameter] public EventCallback IsExportEnabledChanged { get; set; } [Parameter, EditorRequired] public IPivotTableDataSource PivotService { get; set; } = null!; [Parameter] public Navigation? Navigation { get; set; } [Parameter, EditorRequired] public List Collections { get; set; } = null!; [Parameter] public DateTime LastRefresh { get; set { if (field == value) return; field = value; LastRefreshChanged.InvokeAsync(field); } } [Parameter] public TimeSpan LastRefreshElapsed { get; set { if (field == value) return; field = value; LastRefreshElapsedChanged.InvokeAsync(field); } } [Parameter] public EventCallback LastRefreshChanged { get; set; } [Parameter] public EventCallback LastRefreshElapsedChanged { get; set; } [Parameter] public Func? GetPivotDefinition { get; set; } //by default DefaultGetPivotDefinition; [Parameter] public Func?>> GetCustomDrilldown { get; set; }= (_,_) => Task.FromResult?>(null); [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 Func< GroupedPivot /*pivot*/, FilterExpressionTree.ExpressionGroup? /*userFilter*/, List /*pivots*/, NavigationUnit > CreateNavigationUnit { get; set; } = DefaultCreateNavigationUnit; [Parameter] public bool IsReadOnly { get; set; } [Parameter] public bool UseCache { get; set { if (field == value) return; field = value; UseCacheChanged.InvokeAsync(field); } } = true; [Parameter] public EventCallback UseCacheChanged { get; set; } [Parameter] public string Title { get; set; } = ""; [Parameter] public bool ShowIfEmpty { get; set; } = true; [Parameter] public Dictionary HeaderStyles { get; set; } = []; private FilterExpressionTree.ExpressionGroup Filter { get; set; } = new(); public Task CopyCsv() => CopyCsvInternal(); public Task ExportCsv(string destFileName) => ExportCsvInternal(destFileName); public Task RunPivot(bool transpose = false) => IsReadOnly ? Task.CompletedTask : RunPivotInternal(transpose); public void Navigate(PivotDefinition def, NavigationUnit data) { if (IsReadOnly) return; NavigateInternal(def, data); } public Task ShowDefaultDocument(KeyValuePair[] fields) => IsReadOnly ? Task.CompletedTask : ShowDefaultDocumentInternal(fields); private PivotRow[] ? _pivotRows; private IPivotedData ? _pivotData; private readonly MinMaxCache _totals = new(); private string ? _prevCollection; private GroupedCollection ? _currentCollection; private DrilldownSupport ? _drilldownSupport; private bool _pivotRefreshing; private PivotColumnDescriptor[] ? _descriptors; private Dictionary ? _fieldTypes; private List _filteredRows = []; private readonly DelayedExecution _delayedUpdate = new(TimeSpan.FromMilliseconds(1500)); private Dictionary _shadowHeaders = []; private bool ShowTotals => SelectedPivotNode?.Pivot.ShowTotals ?? false; private IMinMaxCache? VisibleTotals => ShowTotals ? _totals : null; private List Pivots => SelectedCollectionNode?.Pivots ?? []; private HashSet AllDataFields => SelectedCollectionNode?.DataFields ?? []; private HashSet AllKeyFields => SelectedCollectionNode?.KeyFields ?? []; private LineConfig ChartConfig => ChartHelper.ChartConfig; private ChartHelperForPivot ChartHelper { get; } = new(); private string ChartClass => !ChartHelper.IsLineChart(CurrentPivot!, _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 != null && (_pivotRows[0].GetDynamicMemberNames().Count() <= 16 || _pivotRows[0].GetDynamicMemberNames().Any(n => n.Length >= 10 && !n.Contains(" ")) ) ) return "table-nowrap"; else return string.Empty; } } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (!firstRender || IsReadOnly) return; try { Navigation ??= new(Navigate); _drilldownSupport = new(Collections) { ShowDocument = ShowDefaultDocument, MessageBoxShow = m => ModalDialogUtils.ShowInfoDialog(Modal, "Pivot", m), MessageBoxShowYesNo = async (m, t) => { var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, t, m); return !res.Cancelled; }, ShowException = ex => ModalDialogUtils.ShowExceptionDialog(Modal, "Pivot", ex), GetPivotDefinition = GetPivotDefinition ?? DefaultGetPivotDefinition, GetCustomDrilldown = GetCustomDrilldown }; await InvokeAsync(StateHasChanged); } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching collections from pivot service", e); } } private PivotDefinition? DefaultGetPivotDefinition(string pivotName, string _ /*collectionName*/) => _currentCollection?.Pivots?.FirstOrDefault(x => x?.Pivot?.Name?.Equals(pivotName, StringComparison.OrdinalIgnoreCase) ?? false)?.Pivot; private async Task OnCellClick(DynamicObject row, string fieldName) { try { if (HandleCellClick != null && await HandleCellClick(row, fieldName)) return true; } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error drilling down into {fieldName}", e); } if ( IsReadOnly || _drilldownSupport == null) return true; if (CurrentPivot == null) return false; var displayToRealName = new Dictionary(StringComparer.OrdinalIgnoreCase); if ( CurrentPivot.RenameColumn?.Count > 0 && PivotData is ArrayBasedPivotData { HeadersMap: { } } apd) { // check for renamed columns foreach ( var item in apd.HeadersMap ) displayToRealName[item.DesplayHeader] = item.OrigHeader; } var res = await _drilldownSupport.Drilldown( PivotService!, SelectedCollectionNode!.CollectionNameWithPrefix, fieldName, displayToRealName, ((PivotRow)row).GetDynamicMemberNames().ToArray(), CurrentPivot, x => TableControl.GetDynamicMember(row, x), AllDataFields, AllKeyFields ); if (res == null) return false; var (destCollection, destPivot) = res; if (destCollection != SelectedCollectionNode.CollectionNameWithPrefix) throw new NotImplementedException("destCollection != Collection"); try { _pivotRefreshing = true; await InvokeAsync(StateHasChanged); await RunPivot(destPivot, Filter); } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); } finally { _pivotRefreshing = false; await InvokeAsync(StateHasChanged); } return true; } private async Task ShowDefaultDocumentInternal(KeyValuePair[] fields) { var filter = ExtraFilter; var doc = await PivotService.GetDocumentAsync(_currentCollection!.CollectionNameWithPrefix, fields, filter); await ModalDialogUtils.ShowTextDialog( Modal, "Pivot", doc, "Press F11 to enter fullscreen mode. Use ESC to exit it." ); } private bool IsRefreshEnabled => ! IsReadOnly && !string.IsNullOrWhiteSpace(SelectedCollectionNode?.CollectionNameWithPrefix) && SelectedPivotNode?.Pivot != null ; private async Task RunPivotInternal(bool transpose) { if (!IsRefreshEnabled) return; _log.Debug($"OnRefreshPivot Collection=\"{SelectedCollectionNode?.CollectionNameWithPrefix}\" Pivot=\"{SelectedPivotNode?.Pivot?.Name}\" ExtraFilter=\"{ExtraFilter}\""); try { var pivot = SelectedPivotNode?.Pivot; if (pivot == null) { await ModalDialogUtils.ShowInfoDialog( Modal, "Pivot", "Pivot is not found.", new() { {"Pivot" , SelectedPivotNode?.Pivot?.Name}, {"Collection" , SelectedCollectionNode?.CollectionNameWithPrefix }, {"ExtraFilter", ExtraFilter?.ToString() } }); return; } // clear previous back/forward stack Navigation?.Clear(); _pivotRefreshing = true; await InvokeAsync(StateHasChanged); await RunPivotInternal(pivot, Filter, transpose: transpose); } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); } finally { _pivotRefreshing = false; await InvokeAsync(StateHasChanged); } } public async Task RunPivot(PivotDefinition pivotDef, FilterExpressionTree.ExpressionGroup? userFilter, bool addToNavigation = true, bool transpose = false) { if (IsReadOnly) return; try { _pivotRefreshing = true; await InvokeAsync(StateHasChanged); await RunPivotInternal(pivotDef, userFilter, addToNavigation, transpose); } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); } finally { _pivotRefreshing = false; await InvokeAsync(StateHasChanged); } } /* ___________________________________________________________________________________________________________ _____ _____ _ _ | __ \ | __ (_) | | | |__) | _ _ __ | |__) |__ _____ | |_ | _ / | | | '_ \| ___/ \ \ / / _ \| __| | | \ \ |_| | | | | | | |\ V / (_) | |_ |_| \_\__,_|_| |_|_| |_| \_/ \___/ \__| ___________________________________________________________________________________________________________ */ private async Task RunPivotInternal( PivotDefinition pivotDef, FilterExpressionTree.ExpressionGroup? userFilter, bool addToNavigation = true, bool transpose = false ) { // userFilter is a combination of drilldown filters and pivot-defined filter // i.e. it excluding an ExtraFilter (the one that set in the UI) if (IsReadOnly) return; if (pivotDef == null) throw new ArgumentNullException(nameof(pivotDef)); if (!await WaitForSelectedCollection()) throw new ApplicationException("SelectedCollectionNode is null"); var sw = Stopwatch.StartNew(); var p = Process.GetCurrentProcess(); var memBefore = p.WorkingSet64; pivotDef = pivotDef.Clone(); _descriptorsCache.Clear(); if (_prevCollection != SelectedCollectionNode!.CollectionNameWithPrefix) { _descriptors = SelectedCollectionNode.ColumnDescriptors; _fieldTypes = SelectedCollectionNode.FieldTypes; _prevCollection = SelectedCollectionNode.CollectionNameWithPrefix; } var fieldTypes = GetAllFields(); pivotDef.Filter = CombineFilters( pivotDef.Filter, userFilter, fieldTypes); var data = await PivotService.PivotAsync(SelectedCollectionNode.CollectionNameWithPrefix, pivotDef, ExtraFilter, !UseCache); if (data.Count == PivotMaxReturnedRows) { await ModalDialogUtils.ShowInfoDialog(Modal, "Pivot Results Truncated", $"Pivot results are limited to returning a maximum of {PivotMaxReturnedRows} rows"); } CurrentPivot = pivotDef; UseCache = true; // reset UseCache _currentCollection = SelectedCollectionNode; if ((pivotDef.RenameColumn?.Count ?? 0) > 0) RenameColumns(data, pivotDef.RenameColumn!, false); data = Make2DPivot(CurrentPivot, data); if (transpose && data.Count > 0 && data.Get(0, 0)?.ToString()?.Equals("No results") != true) data = Transpose(data); PivotData = data; _totals.Clear(); _filteredRows = []; if (addToNavigation) { Navigation?.Add(pivotDef, CreateNavigationUnit(SelectedPivotNode!, userFilter, Pivots)); } sw.Stop(); LastRefresh = DateTime.Now; LastRefreshElapsed = sw.Elapsed; await LastRefreshChanged .InvokeAsync(LastRefresh); await IsExportEnabledChanged .InvokeAsync(IsExportEnabled); p = Process.GetCurrentProcess(); var memAfter = p.WorkingSet64; _log.Debug($"Received Rows={PivotData.Count} Pivot=\"{pivotDef.Name}\" Filter=\"{userFilter}\" MemBefore={NumbersUtils.ToHumanReadable(memBefore)} " + $"MemAfter={NumbersUtils.ToHumanReadable(memAfter)} " + $"MemDiff={NumbersUtils.ToHumanReadable(memAfter - memBefore)} User=\"{PivotService.User}\""); } private async Task WaitForSelectedCollection() { // SelectedCollectionNode can be not set yet if the parent component is setting it // asynchronously. Wait for a while. // This is actually a race condition. Parent initialing Collections loading in background while in the code // we already think that Collections are loaded and SelectedCollectionNode is set. if (SelectedCollectionNode != null) return true; var timeout = TimeSpan.FromSeconds(10); try { var cts = new CancellationTokenSource(timeout); while (SelectedCollectionNode == null && !cts.IsCancellationRequested) await Task.Delay(100, cts.Token); } catch (OperationCanceledException) { // ignore } return SelectedCollectionNode != null; } private string CombineFilters(string pivotFilter, FilterExpressionTree.ExpressionGroup? userFilter, Dictionary fieldTypes) { if (string.IsNullOrWhiteSpace(pivotFilter)) return userFilter?.ToJson(fieldTypes) ?? ""; if (userFilter?.IsEmpty ?? true) return pivotFilter; var baseFilter = FilterExpressionTree.ParseJson(pivotFilter); var resTree = new FilterExpressionTree.ExpressionGroup() { Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And, }; resTree.Children.Add(baseFilter); resTree.Children.Add(userFilter); return resTree.ToJson(fieldTypes); } private void RenameColumns(IPivotedData data, Dictionary pivotDefRenameColumn, bool capitaliseColumns) { if ( data is not ArrayBasedPivotData d ) throw new ApplicationException("ArrayBasedPivotData expected"); var rx = pivotDefRenameColumn.Select(x => new {Rx = new Regex(x.Key), Src=x.Key, Dest = x.Value}).ToArray(); d.UpdateHeaders(header => { foreach (var item in rx) { var m = item.Rx.Match(header); if (!m.Success) continue; var dest = Regex.Replace(header, item.Src, item.Dest); return dest; } if (capitaliseColumns) { if (char.IsLower(header[0]) && header.Length > 1) { return header[0].ToString().ToUpper() + header[1..]; } } return header; }); } private static NavigationUnit DefaultCreateNavigationUnit(GroupedPivot pivot, FilterExpressionTree.ExpressionGroup? userFilter, List pivots ) => new () { Filter = userFilter, Pivots = pivots, SelectedPivotNode = pivot }; private class TransposedData : IPivotedData { private readonly IPivotedData _originalData; private readonly string[] _invertedOrigHeaders; public TransposedData(IPivotedData originalData) { _originalData = originalData; _invertedOrigHeaders = _originalData.Headers.ToArray(); Headers = new [] {_invertedOrigHeaders[0]} .Concat( Enumerable.Range(0, originalData.Count) .Select(x => originalData.Get(0, x)?.ToString() ?? "") ) .ToList(); Id = $"{originalData.Id}-Transposed"; } public string Id { get; set; } public IReadOnlyCollection Headers { get; } public int Count => _originalData.Headers.Count-1; public object? Get(int col, int row) { if (col == 0) { if (row < 0 || row >= _invertedOrigHeaders.Length - 1) return "???"; return _invertedOrigHeaders[row + 1]; } return _originalData.Get(row+1, col-1); } public Type GetColumnType(int col) => typeof(double); public IPivotedData Filter(Func filter) => new TransposedData(_originalData.Filter(filter)); } private IPivotedData Transpose(IPivotedData data) => new TransposedData(data); private async void NavigateInternal(PivotDefinition def, NavigationUnit data) { try { if (IsReadOnly) return; try { _pivotRefreshing = true; Filter = data.Filter ?? new FilterExpressionTree.ExpressionGroup(); SelectedPivotNode = data.SelectedPivotNode; await InvokeAsync(StateHasChanged); await RunPivot(def, null, false); } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e); } finally { _pivotRefreshing = false; await InvokeAsync(StateHasChanged); } } catch (Exception e) { _log.Error(e.Message, e); } } 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; if ( SelectedCollectionNode == null ) return null; _fieldTypes ??= SelectedCollectionNode.FieldTypes; desc = GetFieldDescriptor?.Invoke(columnName); if (desc == null) _fieldTypes?.TryGetValue(columnName, out desc); _fieldDescriptorsCache[columnName] = desc; return desc; } private static IPivotedData Make2DPivot(PivotDefinition def, IPivotedData data) { if (!def.Make2DPivot) return data; var rhArray = (def.Pivot2DRows ?? []) .Select(x => x.Replace("\r", "").Trim()) .ToArray() ; var rowHeaders = new HashSet(rhArray); if ((def.Pivot2DRows?.Count ?? 0) == 0 || rowHeaders.Contains(def.Pivot2DColumn) || rowHeaders.Contains(def.Pivot2DData) || def.Pivot2DColumn == def.Pivot2DData) return data; try { var data2d = new TransposedPivotData(data, def.Pivot2DColumn, rhArray, def.Pivot2DData, CancellationToken.None); return data2d; } catch (Exception) { return data; } } private Dictionary GetAllFields() { if (SelectedCollectionNode?.FieldTypes != null) return SelectedCollectionNode.FieldTypes .ToDictionary( x => x.Key, x => x.Value.Type ); var res = new Dictionary(StringComparer.OrdinalIgnoreCase); if ((AllKeyFields?.Count ?? 0) > 0) { foreach (var k in AllKeyFields!.Where(k => !res.ContainsKey(k))) { res[k] = typeof(string); } } if ((AllDataFields?.Count ?? 0) > 0) { foreach (var k in AllDataFields!.Where(k => !res.ContainsKey(k))) { res[k] = typeof(double); } } // if ((_pivotData?.Length ?? 0) > 0) // { // foreach (var k in _pivotData[0].GetDynamicMemberNames()) // { // if (!res.ContainsKey(k)) // res[k] = typeof(double); // } // } return res; } 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 || !(CurrentPivot?.MakeLineChart ?? false)) return; _delayedUpdate.Run(UpdateFilteredChart, "UpdateFilteredChart"); } } private Task UpdateFilteredChart(CancellationToken token) { try { if (token.IsCancellationRequested) return Task.CompletedTask; var filteredPivot = new PivotFilteredView(_filteredRows.Cast().ToList()); if (token.IsCancellationRequested) return Task.CompletedTask; ChartHelper.UpdateLineChart(CurrentPivot!, 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 cp = CurrentPivot; if ( cp == null || cp.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 * cp.HighlightTopPercent / 100.0 => $"{cellClass} glow-pos", < 0 when val < mm.MinValue - mm.MinValue * cp.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 * CurrentPivot!.HighlightTopPercent / 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; } }