diff --git a/DeepTrace/Controls/CorrelationChart.razor b/DeepTrace/Controls/CorrelationChart.razor new file mode 100644 index 0000000..a7ac8c8 --- /dev/null +++ b/DeepTrace/Controls/CorrelationChart.razor @@ -0,0 +1,204 @@ +@using DeepTrace.Data; +@using DeepTrace.ML; +@using PrometheusAPI; + +@if (_matrix.Count > 0) +{ + + @for (var i = 0; i < _matrix!.Count; i++) + { + + } + +} + +@code { + [CascadingParameter] + protected bool IsDarkMode { get; set; } + + [Parameter] public TimeSeriesData? Data { get; set; } + + private ApexChart? _chart; + private ApexChartOptions? _options; + private List _matrix = new(); + private TimeSeriesData _currentData = new() { Series = { new() } }; + + private record HeatMapData( string Name, float Value) + { + public override string ToString() => $"{Value:N4} {Name}"; + } + private record HeatMapDataSeries(string Name, List Series) + { + public override string ToString() => $"{Name} {Series.Count}"; + } + + protected override async Task OnInitializedAsync() + { + await UpdateChart(); + await base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() + { + await UpdateChart(); + await base.OnParametersSetAsync(); + } + + private decimal GetValue(object o) + { + if (o is float f && !float.IsNaN(f) ) + return (decimal)f; + return 0m; + } + + private async Task UpdateChart() + { + if (Data == _currentData) + return; + + _currentData = Data?.Series.Count > 0 && Data.Series.All( x => x.Data.Count > 0 ) + ? Data + : new() + { + Series = + { + new() + { + Name = "??", + Data = new List + { + new TimeSeries + { + TimeStamp = DateTime.Now, + Value = 0.0F + } + } + } + } + }; + + var matrix = Correlation.Matrix(DataSourceDefinition.Normalize(_currentData.Series) ?? _currentData.Series); + if (matrix.GetLength(0) == 0 ) + { + matrix = new float[_currentData.Series.Count, _currentData.Series.Count]; + } + + _matrix.Clear(); + for( var i = 0; i < matrix.GetLength(0); i++ ) + { + _matrix.Add(new( + TimeSeriesDataSet.MakeLabel(_currentData.Series[i].Name), + Enumerable.Range(0, matrix.GetLength(1)) + .Select(x => new HeatMapData( + TimeSeriesDataSet.MakeLabel(_currentData.Series[x].Name), + matrix[x, i] + )) + .ToList() + ) + ); + } + + _options = CreateOptions(); + + if (_chart == null) + return; + + //await InvokeAsync(StateHasChanged); + await _chart.UpdateSeriesAsync(); + await _chart.UpdateOptionsAsync(true, true, true); + await InvokeAsync(StateHasChanged); + } + + private ApexChartOptions CreateOptions() + { + var backgroundColor = IsDarkMode ? "var(--mud-palette-surface)" : "#f3f3f3"; + var gridColor = IsDarkMode ? "var(--mud-palette-drawer-background)" : "#f3f3f3"; + var borderColor = IsDarkMode ? "var(--mud-palette-text-primary)" : "#e7e7e7"; + var lineColors = _currentData.Series.Select(x => x.Color).ToList(); + var mode = IsDarkMode + ? Mode.Dark + : Mode.Light + ; + + var options = new ApexChartOptions + { + Chart = new() + { + Background = backgroundColor, + Toolbar = new() + { + Show = true + }, + DropShadow = new() + { + Enabled = false, + Color = "", + Top = 18, + Left = 7, + Blur = 10, + Opacity = 0.2d + } + }, + DataLabels = new() + { + Enabled = false + }, + //Tooltip = new ApexCharts.Tooltip + //{ + // Y = new() + // { + // Formatter = @"function(value, opts) { + // if (value === undefined) {return '';} + // return Number(value).toLocaleString();}", + // }, + // X = new() + // { + // Formatter = @"function(value, opts) { + // if (value === undefined) {return '';} + // return (new Date(value)).toISOString();}", + // } + + //}, + //Xaxis = new() + //{ + // Type = XAxisType.Category + //}, + //Grid = new() + //{ + // BorderColor = borderColor, + // Row = new() + // { + // Colors = new List { gridColor, "transparent" }, + // Opacity = 0.5d + // } + //}, + Colors = new List { "#008FFB" }, + //Stroke = new() { Curve = Curve.Straight, Width = 2 }, + //Legend = new() + //{ + // Position = LegendPosition.Top, + // HorizontalAlign = ApexCharts.Align.Right, + // Floating = true, + // OffsetX = -5, + // OffsetY = -25 + //}, + Theme = new() + { + Mode = mode, + //Palette = PaletteType.Palette8, + } + }; + + return options; + } + +} diff --git a/DeepTrace/Controls/TimeSeriesChart.razor b/DeepTrace/Controls/TimeSeriesChart.razor index d6befa7..55d5386 100644 --- a/DeepTrace/Controls/TimeSeriesChart.razor +++ b/DeepTrace/Controls/TimeSeriesChart.razor @@ -1,73 +1,105 @@ @using DeepTrace.Data; @using DeepTrace.Services; @using PrometheusAPI; - using DeepTrace.Data; + + TItem="TimeSeries" + Title="Data view" + Options="@_options" + OnZoomed="OnZoomed" + > @foreach (var ts in _currentData.Series) -{ - -} + } + @code { -[CascadingParameter] -protected bool IsDarkMode { get; set; } + [CascadingParameter] + protected bool IsDarkMode { get; set; } -[Parameter] public TimeSeriesData? Data { get; set; } + [Parameter] public TimeSeriesData? Data { get; set; } -[Parameter] public DateTime? MinDate { get; set; } -[Parameter] public DateTime? MaxDate { get; set; } -[Parameter] public EventCallback MinDateChanged { get; set; } -[Parameter] public EventCallback MaxDateChanged { get; set; } + [Parameter] public DateTime? MinDate { get; set; } + [Parameter] public DateTime? MaxDate { get; set; } + [Parameter] public EventCallback MinDateChanged { get; set; } + [Parameter] public EventCallback MaxDateChanged { get; set; } -private ApexChart? _chart; -private ApexChartOptions? _options; -private TimeSeriesData _currentData = new() { Series = { new () } }; + private ApexChart? _chart; + private ApexChartOptions? _options; + private TimeSeriesData _currentData = CreateEmpty(); -protected override void OnInitialized() -{ - _options = CreateOptions(); - base.OnInitialized(); -} + protected override async Task OnInitializedAsync() + { + await UpdateChart(); + await base.OnInitializedAsync(); + } -protected override async Task OnParametersSetAsync() -{ - Console.WriteLine("OnParametersSet"); + //protected override async Task OnAfterRenderAsync(bool firstRender) + //{ + // if (firstRender) + // await UpdateChart(); + // base.OnAfterRender(firstRender); + //} - await UpdateChart(); - await base.OnParametersSetAsync(); -} + protected override async Task OnParametersSetAsync() + { + await UpdateChart(); + await base.OnParametersSetAsync(); + } -private async Task UpdateChart() -{ - if (Data == _currentData) - return; - _currentData = Data ?? new() { Series = { new() } }; ; - _options = CreateOptions(); + private async Task UpdateChart() + { + if (Data == _currentData) + return; - if (_chart == null) - return; + _currentData = Data?.Series.Count > 0 && Data.Series.All(x => x.Data.Count > 0) + ? Data + : CreateEmpty(); - //await InvokeAsync(StateHasChanged); - await _chart!.UpdateSeriesAsync(); - await _chart!.UpdateOptionsAsync(true, true, true); - await InvokeAsync(StateHasChanged); + _options = CreateOptions(); + + if (_chart == null) + return; + + //await InvokeAsync(StateHasChanged); + if (_currentData.Series.Count > 0) + { + await _chart.UpdateSeriesAsync(); + await _chart.UpdateOptionsAsync(true, true, true); + } + await InvokeAsync(StateHasChanged); + } + + private static TimeSeriesData CreateEmpty() => new() + { + Series = + { + new() + { + Name = "??", + Data = new List + { + new TimeSeries + { + TimeStamp = DateTime.Now, + Value = 0.0F + } + } + } + } + }; -} private ApexChartOptions CreateOptions() { diff --git a/DeepTrace/Data/DataSourceDefinition.cs b/DeepTrace/Data/DataSourceDefinition.cs index 22e4599..8d92552 100644 --- a/DeepTrace/Data/DataSourceDefinition.cs +++ b/DeepTrace/Data/DataSourceDefinition.cs @@ -1,4 +1,7 @@ -namespace DeepTrace.Data; +using PrometheusAPI; +using System.Diagnostics; + +namespace DeepTrace.Data; public class DataSourceQuery { @@ -80,4 +83,109 @@ public class DataSourceDefinition return data; } + + /// + /// Make time series coherent. Timestamps made the same across all series. Values interpolated using linear interpolation. + /// + public static List? Normalize(List source, int nIntervals = 50) + { + if ( source.Count == 0 || source.Any( x => x.Data.Count == 0 ) ) + return null; + + var minTime = source.SelectMany( x => x.Data.Select( y => y.TimeStamp)).Where( x => x != DateTime.MinValue).Min(); + var maxTime = source.SelectMany( x => x.Data.Select( y => y.TimeStamp)).Where( x => x != DateTime.MinValue).Max(); + + if (minTime ==default || maxTime == default) + return null; + + var res = new List(); + var timeInterval = TimeSpan.FromMilliseconds( (double)(maxTime - minTime).TotalMilliseconds / (double)(nIntervals-1) ); + + foreach( var data in source ) + { + static float GetValue(float v) => !float.IsNaN(v) && !float.IsInfinity(v) ? v : 0f; + + if (data.Data.Count == 0) + return null; + + var d = new List(nIntervals); + var dest = new TimeSeriesDataSet + { + Name = data.Name, + Color = data.Color, + Data = d + }; + res.Add(dest); + + var prev = data.Data[0]; // point in time prior to current + if (prev == null) + return null; + var nextIndex = 0; + var prevIndex = 0; + var next = prev; // point in time next to current + + for (var i = 0; i < nIntervals; i++) + { + var ts = minTime + TimeSpan.FromMilliseconds(timeInterval.TotalMilliseconds * i); + + float v; + + if (next.TimeStamp < ts) + { + // if next point timetamp become less than current - move the point forward + for (var idx = nextIndex+1; idx < data.Data.Count; idx++) + { + // skip points if timestamp is in the past + if (data.Data[idx].TimeStamp < ts) + continue; + + // now we sure that point in in future comparing to the current "ts" + nextIndex = idx; + next = data.Data[idx]; + } + + // now try to adjust prev point as there can be point in time closee to current + for (var idx = nextIndex-1; idx >= prevIndex; idx--) + { + // skip points if timestamp is in the past + if (data.Data[idx].TimeStamp > ts) + continue; + + // now we sure that point in in future comparing to the current "ts" + prevIndex = idx; + prev = data.Data[idx]; + } + } + + if (next == prev || next.TimeStamp == ts) + { + v = GetValue(next.Value); + } + else if (prev.TimeStamp == ts) + { + v = GetValue(prev.Value); + } + else + { + //Debug.Assert(ts >= prev.TimeStamp); + //Debug.Assert(ts <= next.TimeStamp); + + // https://stackoverflow.com/questions/8672998/resample-aggregate-and-interpolate-of-timeseries-trend-data + var dt = next.TimeStamp.Subtract(prev.TimeStamp).TotalMilliseconds; + var dv = (double)GetValue(next.Value) - GetValue(prev.Value); + v = (float)(GetValue(prev.Value) + dv * ts.Subtract(prev.TimeStamp).TotalMilliseconds / dt); + } + + + var curr = new TimeSeries( + timeStamp: ts, + value: v + ); + d.Add(curr); + } + } + + return res; + } + } diff --git a/DeepTrace/Data/TimeSeriesData.cs b/DeepTrace/Data/TimeSeriesData.cs index e17a095..027adf0 100644 --- a/DeepTrace/Data/TimeSeriesData.cs +++ b/DeepTrace/Data/TimeSeriesData.cs @@ -1,4 +1,5 @@ using PrometheusAPI; +using System.Reflection.Emit; namespace DeepTrace.Data; @@ -12,4 +13,24 @@ public class TimeSeriesDataSet public string Name { get; init; } = "Value"; public string Color { get; init; } = ""; public List Data { get; init; } = new List(); + + public string Label => MakeLabel(Name); + + public static string MakeLabel(string s) + { + var pos = s.IndexOf("{"); + if (pos > 0) + s = s[..pos]; + pos = s.LastIndexOf("("); + if (pos > 0) + s = s[(pos + 1)..]; + pos = s.LastIndexOf("["); + if (pos > 0) + s = s[..pos]; + pos = s.LastIndexOf(")"); + if (pos > 0) + s = s[..pos]; + + return s; + } } \ No newline at end of file diff --git a/DeepTrace/ML/Correlation.cs b/DeepTrace/ML/Correlation.cs new file mode 100644 index 0000000..5dc38bf --- /dev/null +++ b/DeepTrace/ML/Correlation.cs @@ -0,0 +1,94 @@ +using DeepTrace.Data; + +namespace DeepTrace.ML; + +/// +/// https://xamlbrewer.wordpress.com/2019/03/04/machine-learning-with-ml-net-in-uwp-feature-correlation-analysis/ +/// +public static class Correlation +{ + /// + /// Computes the Pearson Product-Moment Correlation coefficient. + /// + /// Sample data A. + /// Sample data B. + /// The Pearson product-moment correlation coefficient. + /// Original Source: https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Statistics/Correlation.cs + public static float Pearson(IEnumerable dataA, IEnumerable dataB) + { + var n = 0; + var r = 0.0; + + var meanA = 0d; + var meanB = 0d; + var varA = 0d; + var varB = 0d; + + using (IEnumerator ieA = dataA.GetEnumerator()) + using (IEnumerator ieB = dataB.GetEnumerator()) + { + while (ieA.MoveNext()) + { + if (!ieB.MoveNext()) + { + throw new ArgumentOutOfRangeException(nameof(dataB), "Array too short."); + } + + var currentA = ieA.Current; + var currentB = ieB.Current; + + var deltaA = currentA - meanA; + var scaleDeltaA = deltaA / ++n; + + var deltaB = currentB - meanB; + var scaleDeltaB = deltaB / n; + + meanA += scaleDeltaA; + meanB += scaleDeltaB; + + varA += scaleDeltaA * deltaA * (n - 1); + varB += scaleDeltaB * deltaB * (n - 1); + + r += (deltaA * deltaB * (n - 1)) / n; + } + + if (ieB.MoveNext()) + { + throw new ArgumentOutOfRangeException(nameof(dataA), "Array too short."); + } + } + + return (float)(r / Math.Sqrt(varA * varB)); + } + + public static float[,] Matrix(List src) + { + var data = src?.Select(x=> x.Data).ToList(); + var len = data?.Count ?? 0; + + if (data == null || len < 2) + return new float[0,0]; + + var matrix = new float[len, len]; + + + // Populate diagram + for (int x = 0; x < len; ++x) + { + for (int y = 0; y < len - 1 - x; ++y) + { + var seriesA = data[x]; + var seriesB = data[len - 1 - y]; + + var value = Pearson(seriesA.Select(x => x.Value), seriesB.Select(x => x.Value)); + + matrix[x, y ] = value; + matrix[len-1 - y, len-1 - x] = value; + } + + matrix[x, x] = 1; + } + + return matrix; + } +} diff --git a/DeepTrace/Pages/DataSources.razor b/DeepTrace/Pages/DataSources.razor index ca8a012..6a3ef37 100644 --- a/DeepTrace/Pages/DataSources.razor +++ b/DeepTrace/Pages/DataSources.razor @@ -99,10 +99,23 @@ - -