dbMango/Rms.Risk.Mango.Pivot.UI/Pivot/ChartJsHelperForPivot.cs
Alexander Shabarshov 2a7a24c9e7 Initial contribution
2025-11-03 14:43:26 +00:00

371 lines
12 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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.
*/
using System.Collections;
using ChartJs.Blazor.Common.Axes;
using ChartJs.Blazor.Common.Enums;
using ChartJs.Blazor.LineChart;
using Rms.Risk.Mango.Pivot.Core;
using Rms.Risk.Mango.Pivot.UI.Controls;
using Rms.Risk.Mango.Pivot.UI.Services;
namespace Rms.Risk.Mango.Pivot.UI.Pivot;
/// <summary>
/// Prepare LineChard config for the given Pivot definition and data.
/// </summary>
public class ChartHelperForPivot
{
public bool IsLineChart(PivotDefinition pivotDef, IPivotedData pivotData )
=> GetLineChartColumns(pivotDef, pivotData, out _, out _, out _);
public LineConfig ChartConfig { get; } = new()
{
Options = new()
{
Title = new()
{
Display = false,
Text = "Data graph"
},
Scales = new()
{
XAxes =
[
new CategoryAxis
{
ScaleLabel = new()
{
LabelString = "Date"
}
}
],
YAxes =
[
new LinearCartesianAxis
{
ScaleLabel = new()
{
LabelString = "Amount"
}
}
]
},
Tooltips = new()
{
Mode = InteractionMode.Nearest,
Intersect = true
},
Hover = new()
{
Mode = InteractionMode.Nearest,
Intersect = true
},
Responsive = true,
Legend = new()
{
Display = false,
Position = Position.Right,
Labels = new()
{
FontColor = Night.light
}
}
}
};
public void UpdateLineChart(
string name,
IReadOnlyCollection<string> labels,
IReadOnlyCollection<double> data
)
{
if ( name == null || labels == null || data == null )
return;
ChartConfig.Data.Labels.Clear();
ChartConfig.Data.Labels.Add( name );
ChartConfig.Data.Datasets.Clear();
var color = Night.RandomColorString();
var currentDataSet = new LineDataset<double>
{
Label = name,
BackgroundColor = color,
BorderColor = color,
PointBackgroundColor = color,
PointRadius = 3,
PointBorderWidth = 1,
ShowLine = true,
Fill = false,
PointHitRadius = 5,
SteppedLine = SteppedLine.False
};
currentDataSet.AddRange( data );
ChartConfig.Data.XLabels.Clear();
foreach (var label in labels)
ChartConfig.Data.XLabels.Add(label);
ChartConfig.Data.Datasets.Add( currentDataSet );
ChartConfig.Options.Legend ??= new();
ChartConfig.Options.Legend.Display = true;
ChartConfig.Options.Legend.Position = Position.Bottom;
ChartConfig.Options.Scales ??= new();
ChartConfig.Options.Scales?.YAxes.Clear();
if ( data.Count == 0 )
return;
var dataMin = data.Min();
var dataMax = data.Max();
ChartConfig.Options.Scales?.YAxes.Add( new LinearCartesianAxis
{
Ticks = new()
{
Min = dataMin - Math.Abs(dataMin)*0.01, // -1%
Max = dataMax + Math.Abs(dataMax)*0.01 // +1%
}
});
}
public void UpdateLineChart(
PivotDefinition pivotDef,
IPivotedData pivotData,
Func<string, string> getFormat
)
{
if (!GetLineChartColumns(pivotDef, pivotData, out var xCol, out var yCol, out var dataSetColumns))
return;
var comparer = new RowComparer(pivotData, xCol, dataSetColumns!);
// sort row indexes by data set key ( all yCol ), then by X-axis label (xCol)
var orderedRows = Enumerable
.Range(0, pivotData.Count)
.OrderBy(x => x, comparer)
.ToArray()
;
var labelObjects = orderedRows
.Select(x => pivotData.Get(xCol, x))
.Where(x => x != null)
.Distinct()
.OrderBy(x => x)
.ToArray()
;
var labels =labelObjects
.Select( x => TableControl.ConvertToString(x, getFormat(pivotDef.LineChartXAxis!)))
.ToList()
;
if (labels.Count == 0) // impossible
return;
var labelPos = labelObjects
.Select((x, i) => new KeyValuePair<object, int>(x!,i))
.ToDictionary(x => x.Key, x => x.Value)
;
ChartConfig.Data.Labels.Clear();
foreach (var label in labels)
ChartConfig.Data.Labels.Add(label);
ChartConfig.Data.Datasets.Clear();
LineDataset<object?> []? currentDataSet = null;
var currentKey = new object[dataSetColumns?.Count ?? 0];
var rowKey = new object?[dataSetColumns?.Count ?? 0];
var data = new List<object?[]>(Enumerable.Range(0, yCol!.Count).Select(_ => new object[labelObjects.Length]));
var dataMin = double.MaxValue;
var dataMax = double.MinValue;
foreach (var row in orderedRows)
{
for ( var i = 0; i < (dataSetColumns?.Count ?? 0); i++)
rowKey[i] = pivotData.Get(dataSetColumns![i].Item2, row!);
// start new data set if row label (all yCols) changed
if (currentDataSet == null || !rowKey.SequenceEqual(currentKey))
{
if (currentDataSet != null)
{
for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1)
{
currentDataSet[dataSetNo].AddRange(data[dataSetNo]);
ChartConfig.Data.Datasets.Add(currentDataSet[dataSetNo]);
}
data = [..Enumerable.Range(0, yCol.Count).Select(_ => new object[labelObjects.Length])];
}
Array.Copy(rowKey, currentKey, currentKey.Length);
currentDataSet = new LineDataset<object?>[yCol.Count];
for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1)
{
var color = Night.RandomColorString();
currentDataSet[dataSetNo] = new()
{
Label = (yCol.Count > 1 ? $"{yCol[dataSetNo].Item1} - " : "") + string.Join(" - ", currentKey.Select(x => x.ToString())),
BackgroundColor = color,
BorderColor = color,
PointBackgroundColor = color,
PointRadius = 3,
PointBorderWidth = 1,
ShowLine = true,
Fill = pivotDef.LineChartFill,
PointHitRadius = 5,
SteppedLine = pivotDef.LineChartSteppedLine ? SteppedLine.True : SteppedLine.False
};
}
}
// add null if label is missing
var label = pivotData.Get(xCol, row);
if (label == null)
continue;
var pos = labelPos[label];
for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1)
{
var val = pivotData.Get(yCol[dataSetNo].Item2, row);
if (val is double d)
{
if (d < dataMin)
dataMin = d;
if (d > dataMax)
dataMax = d;
}
data[dataSetNo][pos] = val;
}
}
if (currentDataSet != null)
{
for (var dataSetNo = 0; dataSetNo < yCol.Count; dataSetNo += 1)
{
currentDataSet[dataSetNo].AddRange(data[dataSetNo]);
ChartConfig.Data.Datasets.Add(currentDataSet[dataSetNo]);
}
}
ChartConfig.Options.Legend ??= new();
ChartConfig.Options.Legend.Display = pivotDef.LineChartShowLegend;
ChartConfig.Options.Legend.Position = Position.Right;
// ReSharper disable CompareOfFloatsByEqualityOperator
if ( dataMin == double.MaxValue || dataMax == double.MinValue )
return;
ChartConfig.Options.Scales ??= new();
//ChartConfig.Options.Scales.YAxes??= new();
ChartConfig.Options.Scales?.YAxes.Clear();
ChartConfig.Options.Scales?.YAxes.Add( new LinearCartesianAxis
{
Ticks = new()
{
Min = dataMin - Math.Abs(dataMin)*0.01, // -1%
Max = dataMax + Math.Abs(dataMax)*0.01 // +1%
}
});
// ReSharper restore CompareOfFloatsByEqualityOperator
}
private bool GetLineChartColumns(
PivotDefinition pivotDef,
IPivotedData pivotData,
out int xColumn,
out List<Tuple<string, int>>? yColumn,
out List<Tuple<string, int>>? dataSetColumns
)
{
xColumn = -1;
yColumn = null;
dataSetColumns = null;
if ( pivotDef?.MakeLineChart == null
|| pivotData == null
|| pivotData.Count == 0
|| !(pivotData.Headers?.Count > 1)
|| string.IsNullOrWhiteSpace(pivotDef.LineChartXAxis)
|| (pivotDef.LineChartYAxis?.Count ?? 0 ) <= 0
)
{
return false;
}
var headersDict = pivotData.GetColumnPositions();
if (!headersDict.TryGetValue(pivotDef.LineChartXAxis, out var xCol))
return false;
var yCol = (pivotDef.LineChartYAxis ?? [])
.Select(x => headersDict.TryGetValue(x, out var col) ? new Tuple<string, int>(x, col) : null)
.Where(x => x != null)
.ToList()
;
var dCol = (pivotDef.LineChartDataSetKeys ?? [])
.Select(x => headersDict.TryGetValue(x, out var col) ? new Tuple<string, int>(x, col) : null)
.Where(x => x != null)
.ToList()
;
xColumn = xCol;
yColumn = yCol!;
dataSetColumns = dCol!;
return true;
}
/// <summary>
/// Compare pivot rows by multiple columns
/// </summary>
private class RowComparer(IPivotedData pivot, int xCol, List<Tuple<string, int>> dataSetCols) : IComparer<int>
{
private readonly IPivotedData _pivot = pivot;
private readonly int _xCol = xCol;
private readonly List<Tuple<string, int>> _dataSetCols = dataSetCols ?? [];
public int Compare(int row1, int row2)
{
int c;
// sort by data set (if any)
foreach (var (_, col) in _dataSetCols)
{
c = Comparer.Default.Compare(_pivot.Get(col, row1), _pivot.Get(col, row2));
if (c != 0)
return c;
}
// sort by label
c = Comparer.Default.Compare(_pivot.Get(_xCol, row1), _pivot.Get(_xCol, row2));
return c;
}
}
}