//+------------------------------------------------------------------+
//|                                             ScatterPlotChart.mqh |
//+------------------------------------------------------------------+
#ifndef SCATTERPLOTCHART_MQH
#define SCATTERPLOTCHART_MQH

#include <Canvas\Canvas.mqh>
#include "ScatterTypes.mqh"

//+------------------------------------------------------------------+
//| CScatterPlotChart                                                |
//| Renders a duration-versus-profit scatter plot with a regression  |
//| line overlay and a summary footer, using a CCanvas panel.        |
//+------------------------------------------------------------------+
class CScatterPlotChart
  {
private:
   CCanvas               m_canvas;
   string                m_object_name;
   bool                  m_created;
   string                m_font_name;
   int                   m_font_size;
   uint                  m_font_flags;

   uint                  ColorForSymbol(const string symbol, string &known_symbols[], int known_count);
   uint                  HueForIndex(int index) const;
   int                   DurationToX(double duration_minutes, double log_min, double log_range,
                                     int plot_left, int plot_right) const;
   void                  CollectUniqueSymbols(const CTradePoint &trades[], int count,
         string &unique_out[]) const;

public:
                     CScatterPlotChart(void);
                    ~CScatterPlotChart(void);

   bool                  Draw(const CTradePoint &trades[], int count,
                              const CRegressionResult &regression, bool has_regression,
                              const string best_bucket_label, double best_bucket_avg_profit,
                              int x, int y, int width, int height);
   void                  Clear(void);
  };

//+------------------------------------------------------------------+
//| Constructor: assigns a fixed object name for the canvas panel.   |
//| This dashboard only ever creates one scatter panel, so a single  |
//| constant name is sufficient; two instances of this class on the  |
//| same chart would collide and overwrite each other's panel.       |
//+------------------------------------------------------------------+
CScatterPlotChart::CScatterPlotChart(void)
  {
   m_object_name = "ScatterPlotPanel";
   m_created     = false;
   m_font_name   = "Arial";
   m_font_size   = 12;
   m_font_flags  = FW_BOLD;
  }

//+------------------------------------------------------------------+
//| Destructor: intentionally does not remove the canvas panel. The  |
//| panel is a chart object owned by the chart itself, and must      |
//| remain visible after the CScatterPlotChart instance that drew it |
//| goes out of scope at the end of a script's OnStart.              |
//+------------------------------------------------------------------+
CScatterPlotChart::~CScatterPlotChart(void)
  {
  }

//+------------------------------------------------------------------+
//| Draw                                                             |
//| Plots every trade as a dot colored by symbol on a log-scaled     |
//| duration axis, overlays the regression curve when a fit          |
//| succeeded, and prints a summary footer with slope and R^2 (when  |
//| available) alongside the best-performing historical bucket.      |
//+------------------------------------------------------------------+
bool CScatterPlotChart::Draw(const CTradePoint &trades[], int count,
                             const CRegressionResult &regression, bool has_regression,
                             const string best_bucket_label, double best_bucket_avg_profit,
                             int x, int y, int width, int height)
  {
   Clear();
   if(!m_canvas.CreateBitmapLabel(m_object_name, x, y, width, height, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::Print("ScatterDashboard: CreateBitmapLabel failed, error ", ::GetLastError());
      return(false);
     }
   m_created = true;
   m_canvas.FontSet(m_font_name, m_font_size, m_font_flags);
   ::TextSetFont(m_font_name, m_font_size, m_font_flags);
   m_canvas.Erase(::ColorToARGB(clrWhiteSmoke, 255));
   if(count < 1)
     {
      m_canvas.Update();
      return(true);
     }
//--- find the duration and profit range across every trade, with padding
   double min_dur = trades[0].duration_minutes;
   double max_dur = trades[0].duration_minutes;
   double min_pnl = trades[0].profit;
   double max_pnl = trades[0].profit;
   for(int i = 1; i < count; i++)
     {
      if(trades[i].duration_minutes < min_dur)
         min_dur = trades[i].duration_minutes;
      if(trades[i].duration_minutes > max_dur)
         max_dur = trades[i].duration_minutes;
      if(trades[i].profit < min_pnl)
         min_pnl = trades[i].profit;
      if(trades[i].profit > max_pnl)
         max_pnl = trades[i].profit;
     }
   double pnl_range = max_pnl - min_pnl;
   if(pnl_range <= 0.0)
      pnl_range = 1.0;
   double pnl_pad = pnl_range * 0.1;
   min_pnl -= pnl_pad;
   max_pnl += pnl_pad;
   pnl_range = max_pnl - min_pnl;
//--- the duration axis is log-scaled rather than linear, since a handful of
//--- very long trades otherwise crush every ordinary-length trade into a
//--- sliver of the panel; log10(duration + 1) spreads short and long holds
//--- across comparable pixel widths instead of one dominating the axis
   double log_min   = ::MathLog10(min_dur + 1.0);
   double log_max   = ::MathLog10(max_dur + 1.0);
   double log_range = log_max - log_min;
   if(log_range <= 0.0)
      log_range = 1.0;
//--- reserve space for the footer row at the bottom of the panel
   int footer_height = m_font_size + 16;
   int plot_left     = 50;
   int plot_right    = width - 10;
   int legend_left   = plot_left;
//--- collect every distinct symbol first, so the legend's height is known
//--- before the plot area is laid out, and so every dot below can reuse
//--- the same color-to-symbol assignment the legend itself displays
   string unique_symbols[];
   CollectUniqueSymbols(trades, count, unique_symbols);
   int    symbol_count  = ::ArraySize(unique_symbols);
   int    dot_radius    = 5;
   int    legend_gap    = 14;
   int    legend_row_h  = m_font_size + 8;
//--- wrap legend entries onto as many rows as needed for the panel width
   int    legend_rows   = 1;
   int    row_x         = legend_left;
   for(int i = 0; i < symbol_count; i++)
     {
      uint entry_w = 0;
      uint entry_h = 0;
      ::TextGetSize(unique_symbols[i], entry_w, entry_h);
      int needed = (dot_radius * 2) + 6 + (int)entry_w + legend_gap;
      if(row_x + needed > plot_right && row_x > legend_left)
        {
         legend_rows++;
         row_x = legend_left;
        }
      row_x += needed;
     }
   int legend_height = 6 + (legend_rows * legend_row_h);
   int plot_top      = legend_height;
   int plot_bottom   = height - footer_height;
//--- draw the legend itself: one colored dot plus symbol name per entry,
//--- wrapping onto a new row whenever the next entry would run past the
//--- right edge of the plot area
   int legend_y = 6;
   row_x        = legend_left;
   for(int i = 0; i < symbol_count; i++)
     {
      uint entry_w = 0;
      uint entry_h = 0;
      ::TextGetSize(unique_symbols[i], entry_w, entry_h);
      int needed = (dot_radius * 2) + 6 + (int)entry_w + legend_gap;
      if(row_x + needed > plot_right && row_x > legend_left)
        {
         legend_y += legend_row_h;
         row_x     = legend_left;
        }
      uint dot_color = ColorForSymbol(unique_symbols[i], unique_symbols, symbol_count);
      m_canvas.FillCircle(row_x + dot_radius, legend_y + (legend_row_h / 2), dot_radius, dot_color);
      m_canvas.TextOut(row_x + (dot_radius * 2) + 6, legend_y + 2, unique_symbols[i], ::ColorToARGB(clrBlack, 255));
      row_x += needed;
     }
//--- plot every trade as a colored dot, positioned on the log-scaled axis,
//--- reusing the same unique_symbols list so a symbol's dot color always
//--- matches its color in the legend drawn above
   for(int i = 0; i < count; i++)
     {
      int px = DurationToX(trades[i].duration_minutes, log_min, log_range, plot_left, plot_right);
      int py = plot_bottom - (int)(((trades[i].profit - min_pnl) / pnl_range) * (plot_bottom - plot_top));
      uint dot_color = ColorForSymbol(trades[i].symbol, unique_symbols, symbol_count);
      m_canvas.FillCircle(px, py, 4, dot_color);
     }
//--- overlay the regression curve only when a fit actually succeeded. A
//--- failed fit (for example, every trade sharing an identical duration)
//--- still leaves the dots, legend, and best-bucket footer meaningful, so
//--- the whole panel is not thrown away just because the line could not
//--- be drawn
   if(has_regression)
     {
      int    segment_count = 24;
      int    prev_x = 0;
      int    prev_y = 0;
      for(int s = 0; s <= segment_count; s++)
        {
         double log_pos    = log_min + (log_range * s / segment_count);
         double duration_s = ::MathPow(10.0, log_pos) - 1.0;
         double fitted     = regression.intercept + regression.slope * duration_s;
         int    sx = DurationToX(duration_s, log_min, log_range, plot_left, plot_right);
         int    sy = plot_bottom - (int)(((fitted - min_pnl) / pnl_range) * (plot_bottom - plot_top));
         if(s > 0)
            m_canvas.Line(prev_x, prev_y, sx, sy, ::ColorToARGB(clrBlack, 255));
         prev_x = sx;
         prev_y = sy;
        }
     }
//--- label the actual duration range so the log-scaled axis stays readable
   string min_dur_text = ::DoubleToString(min_dur, 0) + " min";
   string max_dur_text = ::DoubleToString(max_dur, 0) + " min";
   m_canvas.TextOut(plot_left, plot_bottom + 2, min_dur_text, ::ColorToARGB(clrGray, 255));
   uint   max_dur_w = 0;
   uint   max_dur_h = 0;
   ::TextGetSize(max_dur_text, max_dur_w, max_dur_h);
   m_canvas.TextOut(plot_right - (int)max_dur_w, plot_bottom + 2, max_dur_text, ::ColorToARGB(clrGray, 255));
//--- draw the summary footer. Slope and R^2 are only drawn when a fit
//--- succeeded; the best-bucket figure is always drawn, since it never
//--- depends on the regression. Each entry's x-position is measured off
//--- the previous one, so the footer stays correctly spaced regardless of
//--- whether it holds two entries or three
   int    footer_y   = plot_bottom + footer_height / 2 + 4;
   int    next_x     = 10;
   uint   measured_w = 0;
   uint   measured_h = 0;
   int    gap        = 16;
   if(has_regression)
     {
      string slope_text = "Slope: " + ::DoubleToString(regression.slope, 3) + "/min";
      string r2_text    = "R^2: " + ::DoubleToString(regression.r_squared, 2);
      m_canvas.TextOut(next_x, footer_y, slope_text, ::ColorToARGB(clrBlack, 255));
      ::TextGetSize(slope_text, measured_w, measured_h);
      next_x += (int)measured_w + gap;
      m_canvas.TextOut(next_x, footer_y, r2_text, ::ColorToARGB(clrBlack, 255));
      ::TextGetSize(r2_text, measured_w, measured_h);
      next_x += (int)measured_w + gap;
     }
   string range_text = "Best bucket: " + best_bucket_label +
                       " (avg " + ::DoubleToString(best_bucket_avg_profit, 2) + ")";
   m_canvas.TextOut(next_x, footer_y, range_text, ::ColorToARGB(clrBlack, 255));
   m_canvas.Update();
   return(true);
  }

//+------------------------------------------------------------------+
//| DurationToX                                                      |
//| Maps a duration in minutes to a pixel x position using a log10   |
//| scale, so trades spanning several orders of magnitude in duration|
//| are spread across the panel instead of clustering near one edge. |
//+------------------------------------------------------------------+
int CScatterPlotChart::DurationToX(double duration_minutes, double log_min, double log_range,
                                   int plot_left, int plot_right) const
  {
   double log_val = ::MathLog10(duration_minutes + 1.0);
   double frac    = (log_val - log_min) / log_range;
   return(plot_left + (int)(frac * (plot_right - plot_left)));
  }

//+------------------------------------------------------------------+
//| CollectUniqueSymbols                                             |
//| Builds a list of every distinct symbol in trades, preserving the |
//| order each symbol first appears in, so the legend and every dot  |
//| color assignment stay consistent with each other.                |
//+------------------------------------------------------------------+
void CScatterPlotChart::CollectUniqueSymbols(const CTradePoint &trades[], int count,
      string &unique_out[]) const
  {
   ::ArrayResize(unique_out, 0);
   for(int i = 0; i < count; i++)
     {
      bool already_seen = false;
      int  n = ::ArraySize(unique_out);
      for(int j = 0; j < n; j++)
        {
         if(unique_out[j] == trades[i].symbol)
           {
            already_seen = true;
            break;
           }
        }
      if(!already_seen)
        {
         ::ArrayResize(unique_out, n + 1);
         unique_out[n] = trades[i].symbol;
        }
     }
  }

//+------------------------------------------------------------------+
//| ColorForSymbol                                                   |
//| Returns a color for the given symbol, looking it up in           |
//| known_symbols so the same symbol always gets the same color. A   |
//| new symbol's color is generated procedurally by HueForIndex()    |
//| rather than drawn from a short fixed list, so distinct colors    |
//| do not run out and start repeating once an account trades more   |
//| than a handful of symbols.                                       |
//+------------------------------------------------------------------+
uint CScatterPlotChart::ColorForSymbol(const string symbol, string &known_symbols[], int known_count)
  {
//--- look for the symbol among those already seen
   for(int i = 0; i < known_count; i++)
     {
      if(known_symbols[i] == symbol)
         return(HueForIndex(i));
     }
//--- a new symbol: append it and generate its color from its index
   ::ArrayResize(known_symbols, known_count + 1);
   known_symbols[known_count] = symbol;
   return(HueForIndex(known_count));
  }

//+------------------------------------------------------------------+
//| HueForIndex                                                      |
//| Generates a color for the given index by stepping hue around the |
//| color wheel by the golden angle (~137.5 degrees) each time. Since|
//| that step is irrational relative to a full 360-degree rotation,  |
//| successive colors stay visually distinct from their neighbors    |
//| far longer than a short fixed palette that repeats exactly every |
//| few symbols. Saturation and value are fixed so every generated   |
//| color stays similarly bright and readable against the panel.     |
//+------------------------------------------------------------------+
uint CScatterPlotChart::HueForIndex(int index) const
  {
   double hue = ::MathMod(index * 137.508, 360.0);
   double sat = 0.65;
   double val = 0.80;
//--- standard HSV to RGB conversion
   double c  = val * sat;
   double x  = c * (1.0 - ::MathAbs(::MathMod(hue / 60.0, 2.0) - 1.0));
   double m  = val - c;
   double r1 = 0.0, g1 = 0.0, b1 = 0.0;
   if(hue < 60.0)
     {
      r1 = c;
      g1 = x;
      b1 = 0.0;
     }
   else
      if(hue < 120.0)
        {
         r1 = x;
         g1 = c;
         b1 = 0.0;
        }
      else
         if(hue < 180.0)
           {
            r1 = 0.0;
            g1 = c;
            b1 = x;
           }
         else
            if(hue < 240.0)
              {
               r1 = 0.0;
               g1 = x;
               b1 = c;
              }
            else
               if(hue < 300.0)
                 {
                  r1 = x;
                  g1 = 0.0;
                  b1 = c;
                 }
               else
                 {
                  r1 = c;
                  g1 = 0.0;
                  b1 = x;
                 }
   uchar r = (uchar)((r1 + m) * 255.0);
   uchar g = (uchar)((g1 + m) * 255.0);
   uchar b = (uchar)((b1 + m) * 255.0);
   return(::ColorToARGB((color)((b << 16) | (g << 8) | r), 255));
  }

//+------------------------------------------------------------------+
//| Clear                                                            |
//| Removes the canvas panel from the chart if it was created.       |
//+------------------------------------------------------------------+
void CScatterPlotChart::Clear(void)
  {
   if(!m_created)
      return;
   m_canvas.Destroy();
   m_created = false;
  }

#endif // SCATTERPLOTCHART_MQH
//+------------------------------------------------------------------+
