using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Media; using DesktopBot.ViewModels; namespace DesktopBot.Controls { /// /// Line chart WPF nativo (nessuna dipendenza esterna). /// Legge una /// e ridisegna automaticamente ad ogni variazione. /// public class PriceLineChart : FrameworkElement { // ── Dependency Properties ──────────────────────────────────────────── public static readonly DependencyProperty PriceDataProperty = DependencyProperty.Register( nameof(PriceData), typeof(IEnumerable), typeof(PriceLineChart), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnPriceDataChanged)); public IEnumerable PriceData { get => (IEnumerable)GetValue(PriceDataProperty); set => SetValue(PriceDataProperty, value); } public static readonly DependencyProperty LineColorProperty = DependencyProperty.Register( nameof(LineColor), typeof(Color), typeof(PriceLineChart), new FrameworkPropertyMetadata(Colors.LimeGreen, FrameworkPropertyMetadataOptions.AffectsRender)); public Color LineColor { get => (Color)GetValue(LineColorProperty); set => SetValue(LineColorProperty, value); } // ── Osserva notifiche della collection ────────────────────────────── private static void OnPriceDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var chart = (PriceLineChart)d; if (e.OldValue is INotifyCollectionChanged oldCol) oldCol.CollectionChanged -= chart.OnCollectionChanged; if (e.NewValue is INotifyCollectionChanged newCol) newCol.CollectionChanged += chart.OnCollectionChanged; chart.InvalidateVisual(); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => InvalidateVisual(); // ── Rendering ─────────────────────────────────────────────────────── protected override void OnRender(DrawingContext dc) { double w = ActualWidth; double h = ActualHeight; if (w < 10 || h < 10) return; // Sfondo dc.DrawRectangle(new SolidColorBrush(Color.FromRgb(0x0E, 0x0E, 0x14)), null, new Rect(0, 0, w, h)); var points = PriceData?.OfType().ToList(); if (points == null || points.Count < 2) { DrawNoData(dc, w, h); return; } const double padLeft = 72; const double padRight = 76; // spazio per la label del prezzo corrente const double padTop = 16; const double padBottom = 32; double chartW = w - padLeft - padRight; double chartH = h - padTop - padBottom; decimal minP = points.Min(p => p.Price); decimal maxP = points.Max(p => p.Price); decimal range = maxP - minP; if (range == 0) range = maxP * 0.01m; // Griglia orizzontale (5 linee) var gridPen = new Pen(new SolidColorBrush(Color.FromArgb(40, 255, 255, 255)), 1); gridPen.Freeze(); for (int i = 0; i <= 4; i++) { double y = padTop + chartH * i / 4.0; dc.DrawLine(gridPen, new Point(padLeft, y), new Point(padLeft + chartW, y)); decimal price = maxP - range * (decimal)(i / 4.0); DrawLabel(dc, price.ToString("N0"), padLeft - 6, y, Color.FromRgb(0x88, 0x88, 0x99), 9, TextAlignment.Right); } // Curva prezzi con fill gradiente var geometry = new StreamGeometry(); using (var ctx = geometry.Open()) { for (int i = 0; i < points.Count; i++) { double x = padLeft + chartW * i / (double)(points.Count - 1); double y = padTop + chartH * (double)((maxP - points[i].Price) / range); if (i == 0) ctx.BeginFigure(new Point(x, y), isFilled: false, isClosed: false); else ctx.LineTo(new Point(x, y), isStroked: true, isSmoothJoin: true); } } geometry.Freeze(); // Fill sotto la curva var fillGeometry = new StreamGeometry(); using (var ctx = fillGeometry.Open()) { double x0 = padLeft; double xN = padLeft + chartW; double yBase = padTop + chartH; ctx.BeginFigure(new Point(x0, yBase), isFilled: true, isClosed: true); for (int i = 0; i < points.Count; i++) { double x = padLeft + chartW * i / (double)(points.Count - 1); double y = padTop + chartH * (double)((maxP - points[i].Price) / range); ctx.LineTo(new Point(x, y), isStroked: false, isSmoothJoin: true); } ctx.LineTo(new Point(xN, yBase), isStroked: false, isSmoothJoin: false); } fillGeometry.Freeze(); var fillBrush = new LinearGradientBrush( Color.FromArgb(80, LineColor.R, LineColor.G, LineColor.B), Color.FromArgb(0, LineColor.R, LineColor.G, LineColor.B), new Point(0, 0), new Point(0, 1)); fillBrush.Freeze(); dc.DrawGeometry(fillBrush, null, fillGeometry); var linePen = new Pen(new SolidColorBrush(LineColor), 1.8); linePen.Freeze(); dc.DrawGeometry(null, linePen, geometry); // Dot sul prezzo corrente (ultimo punto) var last = points.Last(); double lx = padLeft + chartW; double ly = padTop + chartH * (double)((maxP - last.Price) / range); var dotBrush = new SolidColorBrush(LineColor); dotBrush.Freeze(); dc.DrawEllipse(dotBrush, null, new Point(lx, ly), 4, 4); // Label prezzo corrente: disegnata a destra del dot, dentro padRight // Sfondo scuro per leggibilità sul gradiente var priceText = last.Price.ToString("N2"); var priceTf = new FormattedText( priceText, System.Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Consolas"), 10, new SolidColorBrush(LineColor), VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); double labelX = lx + 6; double labelY = ly - priceTf.Height / 2; // Sfondo pill var bgRect = new Rect(labelX - 2, labelY - 1, priceTf.Width + 4, priceTf.Height + 2); dc.DrawRectangle(new SolidColorBrush(Color.FromRgb(0x0E, 0x0E, 0x14)), null, bgRect); dc.DrawText(priceTf, new Point(labelX, labelY)); // Etichette asse X (prime e ultime) if (points.Count >= 2) { DrawLabel(dc, points.First().Timestamp.ToString("HH:mm"), padLeft, padTop + chartH + 6, Color.FromRgb(0x66, 0x66, 0x77), 9, TextAlignment.Left); DrawLabel(dc, points.Last().Timestamp.ToString("HH:mm"), padLeft + chartW, padTop + chartH + 6, Color.FromRgb(0x66, 0x66, 0x77), 9, TextAlignment.Right); } } private static void DrawNoData(DrawingContext dc, double w, double h) { var tf = new FormattedText( "In attesa dei dati...", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 13, new SolidColorBrush(Color.FromRgb(0x55, 0x55, 0x66)), VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); dc.DrawText(tf, new Point((w - tf.Width) / 2, (h - tf.Height) / 2)); } private static void DrawLabel(DrawingContext dc, string text, double x, double y, Color color, double size, TextAlignment align) { var tf = new FormattedText( text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Consolas"), size, new SolidColorBrush(color), VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); tf.TextAlignment = align; dc.DrawText(tf, new Point(x, y - tf.Height / 2)); } } }