using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using FasdDesktopUi.Basics.Models; using static C4IT.Logging.cLogManager; namespace FasdDesktopUi.Basics.UserControls { public partial class DynamicChart : UserControl { #region Variables int DataDurationStart; int DataDurationEnd; int DataDurationTotal; int timeSkip; private double PaddingTop; private double PaddingBottom; private double PaddingLeft; private double PaddingRight; private double DataHeight; private double DataWidth; private LinearTransformation xCoordinate; private LinearTransformation yCoordinate; Size sizeY; Size sizeX; #endregion #region Properties #region ChartData public cChartModel ChartData { get { return (cChartModel)GetValue(ChartDataProperty); } set { SetValue(ChartDataProperty, value); } } public static readonly DependencyProperty ChartDataProperty = DependencyProperty.Register("ChartData", typeof(cChartModel), typeof(DynamicChart), new PropertyMetadata(null, new PropertyChangedCallback(HandleDependancyPropertyChanged))); private static void HandleDependancyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DynamicChart me)) return; me.BindingValueChange(); } #endregion #region IsCloseButtonVisible public bool IsCloseButtonVisible { get { return (bool)GetValue(IsCloseButtonVisibleProperty); } set { SetValue(IsCloseButtonVisibleProperty, value); } } public static readonly DependencyProperty IsCloseButtonVisibleProperty = DependencyProperty.Register("IsCloseButtonVisible", typeof(bool), typeof(DynamicChart), new PropertyMetadata(false)); #endregion #region IsBarChart public bool isBarChart { get { return (bool)GetValue(isBarChartProperty); } set { SetValue(isBarChartProperty, value); } } // Using a DependencyProperty as the backing store for isBarChart. This enables animation, styling, binding, etc... public static readonly DependencyProperty isBarChartProperty = DependencyProperty.Register("isBarChart", typeof(bool), typeof(DynamicChart), new PropertyMetadata(true, new PropertyChangedCallback(HandleBarChartPropertyChanged))); private static void HandleBarChartPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!(d is DynamicChart me)) return; me.ChartTypeChange(); } #endregion #region GraphicDataProperty public List GraphicDataProperty { get; set; } = new List(); #endregion #endregion #region Internal Classes public class cChartValue { public DateTime Time { get; set; } public double Value { get; set; } public int Duration { get; set; } } public class LinearTransformation { private double x0; private double x1; private double z0; private double z1; private double a; private double c; public LinearTransformation(double x0, double x1, double z0, double z1) { this.x0 = x0; this.x1 = x1; this.z0 = z0; this.z1 = z1; a = (x1 - x0) / (z1 - z0); c = x0 - (a * z0); } public double GetCoordinate(double x) { return a * x + c; } } #endregion public Action CloseButtonClickedAction { get; set; } #region Restructure public void BindingValueChange() { try { ClearControl(); if (ChartData == null) { return; } if (this.ActualHeight == 0 || this.ActualWidth == 0) { return; } SetGraphicDataProperty(); SetTitle(); SetDate(); SetTime(); SetCanvas(); SetScale(); SetTimeStamps(); SetLines(); if (isBarChart == true) { SetValuesBarChart(); } else { SetValuesLineGraph(); } } catch (Exception E) { LogException(E); } } public void ChartTypeChange() { try { GraphicCanvas.Children.Clear(); if (isBarChart == true) { BarChartButton.Visibility = Visibility.Collapsed; LineGraphButton.Visibility = Visibility.Visible; SetScale(); SetTimeStamps(); SetLines(); SetValuesBarChart(); } else { BarChartButton.Visibility = Visibility.Visible; LineGraphButton.Visibility = Visibility.Collapsed; SetScale(); SetTimeStamps(); SetLines(); SetValuesLineGraph(); } } catch (Exception E) { LogException(E); } } #endregion #region Methods public void SetGraphicDataProperty() { try { if (ChartData?.Data == null) return; var reverseData = ChartData.Data.ToList(); reverseData.Reverse(); if ((DateTime)reverseData[0][ChartData.TimeIndex] < (DateTime)reverseData[reverseData.Count() - 1][ChartData.TimeIndex]) { foreach (var item in reverseData) { if (item[ChartData.ValueIndex] == null) { continue; } cChartValue data = new cChartValue(); data.Value = (double)Convert.ChangeType(item[ChartData.ValueIndex], typeof(double)); if (ChartData.DurationIndex > -1) data.Duration = int.Parse(item[ChartData.DurationIndex].ToString()); data.Time = (DateTime)item[ChartData.TimeIndex]; data.Time = data.Time.ToLocalTime(); GraphicDataProperty.Add(data); } } else if ((DateTime)ChartData.Data[0][ChartData.TimeIndex] < (DateTime)ChartData.Data[ChartData.Data.Count() - 1][ChartData.TimeIndex] || ChartData.Data.Count == 1) { foreach (var item in ChartData.Data) { if (item[ChartData.ValueIndex] == null) { continue; } cChartValue data = new cChartValue(); data.Value = (double)Convert.ChangeType(item[ChartData.ValueIndex], typeof(double)); if (ChartData.DurationIndex > -1) data.Duration = int.Parse(item[ChartData.DurationIndex].ToString()); data.Time = (DateTime)item[ChartData.TimeIndex]; data.Time = data.Time.ToLocalTime(); GraphicDataProperty.Add(data); } } } catch (Exception E) { LogException(E); } } public void ClearControl() { TitleBlock.Text = string.Empty; DateBlock.Text = string.Empty; GraphicCanvas.Children.Clear(); GraphicDataProperty.Clear(); } public void SetTitle() { TitleBlock.Text = ChartData.ChartTitle; } public void SetDate() { try { DateTime dataDate = GraphicDataProperty.First().Time; dataDate = dataDate.Date; DateBlock.Text = $"{dataDate.ToString("dddd", new CultureInfo(cFasdCockpitConfig.Instance.SelectedLanguage))}, {dataDate.ToString("d", new CultureInfo(cFasdCockpitConfig.Instance.SelectedLanguage))}"; } catch (Exception E) { LogException(E); } } public void SetTime() { try { DateTime firstTime = GraphicDataProperty.First().Time; DateTime lastTime = GraphicDataProperty.Last().Time; int lastDuration = GraphicDataProperty.Last().Duration; lastTime = lastTime.AddMilliseconds(lastDuration); DataDurationStart = firstTime.Hour; DataDurationEnd = lastTime.Hour + 1; TimeSpan totalDuration = lastTime - firstTime; DataDurationTotal = DataDurationEnd - DataDurationStart + (24 * totalDuration.Days); DataDurationTotal = GetNextLogicalTotalDuration(DataDurationTotal); // temp fix DataDurationEnd = DataDurationStart + DataDurationTotal; timeSkip = (int)Math.Ceiling(DataDurationTotal / 5.0); } catch (Exception E) { LogException(E); } // this is a temporary helper method to keep the structure of the current state of the charts. // however the logic should be rethought and maybe finally replaced for a even more dynamic approach int GetNextLogicalTotalDuration(int totalDuration) { if (totalDuration < 0) return 24 + totalDuration; else if (totalDuration <= 3) return 3; else if (totalDuration <= 5) return 5; else if (totalDuration <= 8) return 8; else if (totalDuration <= 10) return 10; else if (totalDuration <= 12) return 12; else if (totalDuration <= 15) return 15; else if (totalDuration <= 18) return 18; else if (totalDuration <= 20) return 20; else return 24; } } public void SetCanvas() { try { string textY; if (ChartData.UnitFormat.Contains("{0}")) { textY = String.Format(ChartData.UnitFormat, ChartData.MaxValue.ToString()); } else { textY = ChartData.MaxValue.ToString() + ChartData.UnitFormat; } sizeY = GetStringSize(textY, 11, new System.Windows.Media.FontFamily("Calibri")); string textX = "09:00"; sizeX = GetStringSize(textX, 11, new System.Windows.Media.FontFamily("Calibri")); PaddingTop = 5 + (sizeY.Height / 2); PaddingRight = 10 + sizeY.Width; PaddingBottom = 10 + sizeX.Height + PaddingTop; PaddingLeft = 10 + (sizeX.Width / 2); DataHeight = GraphicCanvas.ActualHeight - PaddingTop - PaddingBottom; DataWidth = GraphicCanvas.ActualWidth - PaddingLeft - PaddingRight; yCoordinate = new LinearTransformation(PaddingTop + DataHeight, PaddingTop, ChartData.MinValue, ChartData.MaxValue); xCoordinate = new LinearTransformation(PaddingLeft, PaddingLeft + DataWidth, DataDurationStart * 3600000, (DataDurationStart + DataDurationTotal) * 3600000); } catch (Exception E) { LogException(E); } } public void SetScale() { try { int Minimum = 0; if ((ChartData.MinValue / ChartData.StepLengthScale) % 1 != 0) { int Multiplikator = ChartData.MinValue / ChartData.StepLengthScale; Multiplikator++; Minimum = Multiplikator * ChartData.MinValue; } else { Minimum = ChartData.MinValue; } for (int i = Minimum; i <= ChartData.MaxValue; i += ChartData.StepLengthScale) { string text; if (ChartData.UnitFormat.Contains("{0}")) { text = String.Format(ChartData.UnitFormat, i.ToString()); } else { text = i.ToString() + ChartData.UnitFormat; } TextBlock textBlock = new TextBlock() { Text = text, FontFamily = new FontFamily("Calibri"), FontSize = 11, }; textBlock.SetResourceReference(TextBlock.ForegroundProperty, "Color.Menu.Icon"); Canvas.SetTop(textBlock, yCoordinate.GetCoordinate(i) - (sizeY.Height / 2)); Canvas.SetLeft(textBlock, PaddingLeft + DataWidth + 1); GraphicCanvas.Children.Add(textBlock); } } catch (Exception E) { LogException(E); } } public void SetTimeStamps() { try { for (int i = DataDurationStart; i <= DataDurationEnd; i++) { if ((DataDurationStart - i) % timeSkip != 0) { continue; } string timeStamp; int timeStampTime = i; if (timeStampTime > 24) { timeStampTime -= 24; } if (i < 10) { timeStamp = "0" + timeStampTime.ToString() + ":00"; } else { timeStamp = timeStampTime.ToString() + ":00"; } TextBlock textBlock = new TextBlock() { Text = timeStamp, FontFamily = new FontFamily("Calibri"), FontSize = 11, }; textBlock.SetResourceReference(TextBlock.ForegroundProperty, "Color.Menu.Icon"); Canvas.SetTop(textBlock, PaddingTop + DataHeight + (sizeX.Height / 2)); Canvas.SetLeft(textBlock, (xCoordinate.GetCoordinate(i * 3600000)) - (sizeX.Width / 2)); GraphicCanvas.Children.Add(textBlock); } } catch (Exception E) { LogException(E); } } public void SetLines() { try { int Minimum = 0; if ((ChartData.MinValue / ChartData.StepLengthLine) % 1 != 0) { int Multiplikator = ChartData.MinValue / ChartData.StepLengthLine; Multiplikator++; Minimum = Multiplikator * ChartData.MinValue; } else { Minimum = ChartData.MinValue; } for (int i = Minimum; i <= ChartData.MaxValue; i += ChartData.StepLengthLine) { Point point1 = new Point(PaddingLeft, yCoordinate.GetCoordinate(i)); Point point2 = new Point(PaddingLeft + DataWidth, yCoordinate.GetCoordinate(i)); Line dottedLine = new Line(); dottedLine.SetResourceReference(Line.StrokeProperty, "HorizontalLineColor"); dottedLine.StrokeThickness = 1; dottedLine.StrokeDashArray = new DoubleCollection { 5, 5 }; dottedLine.X1 = point1.X; dottedLine.Y1 = point1.Y; dottedLine.X2 = point2.X; dottedLine.Y2 = point2.Y; GraphicCanvas.Children.Add(dottedLine); } } catch (Exception E) { LogException(E); } } public void SetValuesLineGraph() { try { foreach (var data in GraphicDataProperty) { DateTime dataTime = data.Time; double dataValue = data.Value; int dataDuration = data.Duration; double time = dataTime.TimeOfDay.TotalMilliseconds; double xLeft = xCoordinate.GetCoordinate(time); double xRight = xCoordinate.GetCoordinate(time + dataDuration); double y = 0; if (dataValue > ChartData.MaxValue) { y = yCoordinate.GetCoordinate(ChartData.MaxValue) - 5; } else if (dataValue < ChartData.MinValue) { y = yCoordinate.GetCoordinate(ChartData.MinValue) + 5; } else { y = yCoordinate.GetCoordinate(dataValue); } Border border = new Border() { Height = 6.0, Width = xRight - xLeft, CornerRadius = new CornerRadius(4.0), ToolTip = dataTime.ToShortTimeString() + " - " + dataTime.AddMilliseconds(dataDuration).ToShortTimeString() + " | " + dataValue, }; if (border.Width < 6) { border.Width = 6; } if (ChartData.IsThresholdActive == false) { border.Background = new SolidColorBrush(Color.FromRgb(0, 157, 221)); } else { if (ChartData.IsDirectionUp == false) { if (dataValue <= ChartData.ErrorThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(206, 61, 54)); } else if (dataValue <= ChartData.WarningThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(251, 157, 40)); } else { border.Background = new SolidColorBrush(Color.FromRgb(117, 177, 89)); } } else { if (dataValue >= ChartData.ErrorThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(206, 61, 54)); } else if (dataValue >= ChartData.WarningThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(251, 157, 40)); } else { border.Background = new SolidColorBrush(Color.FromRgb(117, 177, 89)); } } } Canvas.SetTop(border, y - (border.Height / 2)); Canvas.SetLeft(border, xLeft); GraphicCanvas.Children.Add(border); } } catch (Exception E) { LogException(E); } } public void SetValuesBarChart() { try { foreach (var data in GraphicDataProperty) { DateTime dataTime = data.Time; double dataValue = data.Value; int dataDuration = data.Duration; double time = dataTime.TimeOfDay.TotalMilliseconds; double xLeft = xCoordinate.GetCoordinate(time); double xRight = xCoordinate.GetCoordinate(time + dataDuration); double y = 0; if (dataValue > ChartData.MaxValue) { y = yCoordinate.GetCoordinate(ChartData.MaxValue) - 5; } else if (dataValue < ChartData.MinValue) { y = yCoordinate.GetCoordinate(ChartData.MinValue) + 5; } else { y = yCoordinate.GetCoordinate(dataValue); } Border border = new Border() { Height = yCoordinate.GetCoordinate(0) - y, Width = xRight - xLeft, CornerRadius = new CornerRadius(2, 2, 0, 0), ToolTip = dataTime.ToShortTimeString() + " - " + dataTime.AddMilliseconds(dataDuration).ToShortTimeString() + " | " + dataValue, }; if (border.Width < 6) { border.Width = 6; } if (ChartData.IsThresholdActive == false) { border.Background = new SolidColorBrush(Color.FromRgb(0, 157, 221)); } else { if (ChartData.IsDirectionUp == false) { if (dataValue <= ChartData.ErrorThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(206, 61, 54)); } else if (dataValue <= ChartData.WarningThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(251, 157, 40)); } else { border.Background = new SolidColorBrush(Color.FromRgb(117, 177, 89)); } } else { if (dataValue >= ChartData.ErrorThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(206, 61, 54)); } else if (dataValue >= ChartData.WarningThreshold) { border.Background = new SolidColorBrush(Color.FromRgb(251, 157, 40)); } else { border.Background = new SolidColorBrush(Color.FromRgb(117, 177, 89)); } } } Canvas.SetTop(border, y); Canvas.SetLeft(border, xLeft); GraphicCanvas.Children.Add(border); } } catch (Exception E) { LogException(E); } } #endregion public DynamicChart() { InitializeComponent(); } #region CloseButton_Click private void CloseButton_Click() { if (CloseButtonClickedAction != null) CloseButtonClickedAction.Invoke(); } private void CloseButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { CloseButton_Click(); } private void CloseButton_TouchDown(object sender, TouchEventArgs e) { CloseButton_Click(); } #endregion #region ChangeChartType_Click private void ChangeChartType_Click(object sender) { if (!(sender is FrameworkElement senderElement)) { return; } if (senderElement.Name == "BarChartButton") { isBarChart = true; } if (senderElement.Name == "LineGraphButton") { isBarChart = false; } } private void ChangeChartType_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { ChangeChartType_Click(sender); } private void ChangeChartType_TouchDown(object sender, TouchEventArgs e) { ChangeChartType_Click(sender); } #endregion private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) { BindingValueChange(); } public static Size GetStringSize(string text, double fontSize, FontFamily fontFamily) { CultureInfo cultureInfo = CultureInfo.GetCultureInfo("de-DE"); FlowDirection flowDirection = FlowDirection.LeftToRight; #pragma warning disable CS0618 // Type or member is obsolete FormattedText formattedText = new FormattedText( text, cultureInfo, flowDirection, new Typeface(fontFamily.Source), fontSize, Brushes.Black ); #pragma warning restore CS0618 // Type or member is obsolete return new Size(formattedText.Width, formattedText.Height); } } }