preview
MQL5 Trading Tools (Part 13): Creating a Canvas-Based Price Dashboard with Graph and Stats Panels

MQL5 Trading Tools (Part 13): Creating a Canvas-Based Price Dashboard with Graph and Stats Panels

MetaTrader 5Trading systems |
292 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In our previous article (Part 12), we enhanced the correlation matrix dashboard in MetaQuotes Language 5 (MQL5) with interactive features for greater usability. In Part 13, we present a canvas-based price dashboard featuring graph and stats panels, designed to provide traders with clear, actionable insights into price movements and account metrics. Utilizing the CCanvas class, this dashboard offers draggable and resizable panels, visually tracks recent price graphs, and displays vital statistics, including balance, equity, and the current bar’s open/high/low/close values. It further enriches our trading decisions by using customizable backgrounds, theme toggling, and real-time updates. These tools enable you to monitor and respond to market changes more efficiently. We will cover the following topics:

  1. Understanding the Canvas-Based Price Dashboard Framework
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you’ll have a functional MQL5 canvas-based dashboard for monitoring prices and metrics, ready for customization—let’s dive in!


Understanding the Canvas-Based Price Dashboard Framework

The canvas-based price dashboard framework leverages the CCanvas class in MQL5 to create custom graphical panels for displaying real-time price data and account metrics, offering a compact, interactive alternative to standard chart indicators for users needing quick visual overviews without cluttering the main chart. It consists of a main graph panel plotting recent bar closes as a line with filled areas and fog effects for depth, an optional stats panel showing account details like balance/equity and current bar OHLC, both supported by background images with opacity blending, gradient or solid fills, and double borders for aesthetics.

Enhancements include mouse-driven dragging for repositioning, resizing via border hovers and grips with icons for feedback, minimize/maximize toggles to collapse panels, theme switching between dark/light modes for color adjustments, and real-time updates on new bars to reflect the latest prices and stats.

These features use event handling for mouse interactions, bicubic scaling for smooth image resizing, alpha blending for overlays like fog, and ARGB color management for transparency, ensuring the dashboard is responsive and customizable without native MQL5 objects, which was our main goal since we have used the native objects a couple of times now; we change the approach this time round and fully explore the canvas features instead.

Our plan is to include the canvas library, define inputs for positions/sizes/colors/opacities/modes, load and scale a background image resource, create separate canvases for header/graph/stats with creation checks, implement drawing functions for headers with icons/tooltips/borders, graphs with price plotting/filling/time labels/resize icons, and stats with themed text/gradients/darkened borders, add helper functions for color interpolation/darkening/blending/ARGB extraction, and handle chart events for hovers/drags/resizes/toggles with clamping/minimum sizes, updating on ticks for new data. In brief, here is a visual representation of our objectives.

CANVAS DASHBOARD FRAMEWORK


Implementation in MQL5

To create the program in MQL5, open the MetaEditor, go to the Navigator, locate the Experts folder, click on the "New" tab, and follow the prompts to create the file. Once it is made, in the coding environment, we will need to declare some input parameters and global variables that we will use throughout the program.

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART1.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                                             //--- Declare canvas for graph
CCanvas canvasStats;                                             //--- Declare canvas for stats
CCanvas canvasHeader;                                            //--- Declare canvas for header

string canvasGraphName = "GraphCanvas";                          //--- Set graph canvas name
string canvasStatsName = "StatsCanvas";                          //--- Set stats canvas name
string canvasHeaderName = "HeaderCanvas";                        //--- Set header canvas name

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int graphBars                   = 50;                      // Number of recent bars to plot in the graph
input color borderColor               = clrBlack;                // Border color (change for white chart background)
input color borderHoverColor          = clrRed;                  // Border color on hover for resize indication

input int CanvasX                     = 30;                      // Main canvas X position
input int CanvasY                     = 50;                      // Main canvas Y position
input int CanvasWidth                 = 400;                     // Main canvas width
input int CanvasHeight                = 300;                     // Main canvas height

input bool EnableStatsPanel           = true;                    // Enable second stats panel
input int PanelGap                    = 10;                      // Gap between panels in pixels

input bool UseBackground              = true;                    // Enable background image
input double FogOpacity               = 0.5;                     // Fog opacity (0.0 = no fog/fully transparent, 1.0 = fully opaque)
input bool BlendFog                   = true;                    // Blend fog with image (true: image visible under fog; false: original fog hides image)

input int StatsFontSize               = 12;                      // Font size for stats panel text
input color StatsLabelColor           = clrDodgerBlue;           // Color for stats labels
input color StatsValueColor           = clrWhite;                // Color for stats values
input color StatsHeaderColor          = clrDodgerBlue;           // Color for stats headers
input int StatsHeaderFontSize         = 14;                      // Font size for stats headers

input double BorderOpacityPercentReduction = 20.0;               // Percent reduction for border opacity (0-100)
input double BorderDarkenPercent      = 30.0;                    // Percent to darken borders (0-100)

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_BACKGROUND_MODE
{
   NoColor          = 0,         // No color fill
   SingleColor      = 1,         // Single color fill
   GradientTwoColors = 2         // Gradient with two colors
};

input ENUM_BACKGROUND_MODE StatsBackgroundMode = GradientTwoColors; // Stats background mode
input color TopColor                   = clrBlack;                  // Top color for gradient or single fill
input color BottomColor                = clrRed;                    // Bottom color for gradient
input double BackgroundOpacity         = 0.7;                       // Opacity for stats background fill (0.0 to 1.0)

enum ENUM_RESIZE_MODE
{
   NONE,
   BOTTOM,
   RIGHT,
   BOTTOM_RIGHT
};

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "1. Transparent MT5 bmp image.bmp"                     // Hardcoded background image resource

//+------------------------------------------------------------------+
//| Global Variables Continued                                       |
//+------------------------------------------------------------------+
uint original_bg_pixels[];                                       //--- Declare original unscaled background
uint orig_w = 0, orig_h = 0;                                     //--- Initialize original dimensions
uint bg_pixels_graph[];                                          //--- Declare scaled background for graph
uint bg_pixels_stats[];                                          //--- Declare scaled background for stats

int currentCanvasX = CanvasX;                                    //--- Set current X position
int currentCanvasY = CanvasY;                                    //--- Set current Y position
int currentWidth = CanvasWidth;                                  //--- Set current width
int currentHeight = CanvasHeight;                                //--- Set current height
bool panel_dragging = false;                                     //--- Set dragging flag
int panel_drag_x = 0, panel_drag_y = 0;                          //--- Initialize drag start mouse coordinates
int panel_start_x = 0, panel_start_y = 0;                        //--- Initialize drag start panel coordinates
bool resizing = false;                                           //--- Set resizing flag
ENUM_RESIZE_MODE resize_mode = NONE;                             //--- Set resize mode
ENUM_RESIZE_MODE hover_mode = NONE;                              //--- Set hover mode for resize
int resize_start_x = 0, resize_start_y = 0;                      //--- Initialize resize start coordinates
int start_width = 0, start_height = 0;                           //--- Initialize start dimensions
const int resize_thickness = 5;                                  //--- Set resize border thickness
const int min_width = 200;                                       //--- Set minimum width
const int min_height = 150;                                      //--- Set minimum height
int hover_mouse_local_x = 0;                                     //--- Set local mouse x for icon
int hover_mouse_local_y = 0;                                     //--- Set local mouse y for icon
bool header_hovered = false;                                     //--- Set header hover flag
bool minimize_hovered = false;                                   //--- Set minimize hover flag
bool close_hovered = false;                                      //--- Set close hover flag
bool theme_hovered = false;                                      //--- Set theme hover flag
bool resize_hovered = false;                                     //--- Set resize hover flag
int prev_mouse_state = 0;                                        //--- Initialize previous mouse state
int last_mouse_x = 0, last_mouse_y = 0;                          //--- Initialize last mouse position
int header_height = 27;                                          //--- Set header height
int gap_y = 7;                                                   //--- Set y gap
int button_size = 25;                                            //--- Set button size
int theme_x_offset = -75;                                        //--- Set theme offset relative to header right
int minimize_x_offset = -50;                                     //--- Set minimize offset relative to header right
int close_x_offset = -25;                                        //--- Set close offset relative to header right
bool panels_minimized = false;                                   //--- Set minimized flag
color HeaderColor = C'60,60,60';                                 //--- Set header color
color HeaderHoverColor = clrRed;                                 //--- Set header hover color
color HeaderDragColor = clrMediumBlue;                           //--- Set header drag color

bool is_dark_theme = true;                                       //--- Set dark theme flag

color LightHeaderColor = clrSilver;                              //--- Set light header color
color LightHeaderTextColor = clrBlack;                           //--- Set light header text color
color LightStatsLabelColor = clrBlue;                            //--- Set light stats label color
color LightStatsValueColor = clrBlack;                           //--- Set light stats value color
color LightStatsHeaderColor = clrBlue;                           //--- Set light stats header color
color LightBorderColor = clrBlack;                               //--- Set light border color
color LightTopColor = clrGreen;                                  //--- Set light top color
color LightBottomColor = clrGold;                                //--- Set light bottom color
color LightHeaderHoverColor = clrRed;                            //--- Set light header hover color
color LightHeaderDragColor = clrMediumBlue;                      //--- Set light header drag color

bool graphCreated = false;                                       //--- Set graph created flag
bool statsCreated = false;                                       //--- Set stats created flag

We begin the implementation by including the canvas library with "#include <Canvas/Canvas.mqh>", which provides the CCanvas class for creating and managing bitmap-based graphical panels on the chart. We then declare three "CCanvas" objects: "canvasGraph" for the price graph panel, "canvasStats" for the statistics panel, and "canvasHeader" for the header section. We set string constants for their names as "GraphCanvas", "StatsCanvas", and "HeaderCanvas" to identify them uniquely.

Next, we define input parameters to customize the dashboard: "graphBars" as fifty for the number of bars to plot, "borderColor" to black and "borderHoverColor" to red for borders and hover indications, positions and dimensions like "CanvasX" at thirty, "CanvasY" at fifty, "CanvasWidth" at four hundred, "CanvasHeight" at three hundred, a boolean "EnableStatsPanel" true to show the stats panel with "PanelGap" at ten pixels, "UseBackground" true for image backgrounds with "FogOpacity" at 0.5 and "BlendFog" true for blending, font sizes and colors for stats like "StatsFontSize" at twelve, "StatsLabelColor" to dodger blue, and percentages for border adjustments such as "BorderOpacityPercentReduction" at 20.0 and "BorderDarkenPercent" at 30.0.

We create the "ENUM_BACKGROUND_MODE" enumeration with options "NoColor" for no fill, "SingleColor" for uniform color, and "GradientTwoColors" for two-color gradients, with input "StatsBackgroundMode" defaulting to gradient, and colors "TopColor" to black, "BottomColor" to red, plus "BackgroundOpacity" at 0.7. Then, we add the "ENUM_RESIZE_MODE" enumeration for resizing states: "NONE", "BOTTOM", "RIGHT", and "BOTTOM_RIGHT".

We include a resource with the #resource directive; "1. Transparent MetaTrader 5 bmp image.bmp" for a hardcoded background image. The image you attach needs to be a bitmap file only. You can easily do that, and our image file now looks like this. We imported it to where the program file is for simplicity. Have a look at what we got below.

BITMAP IMAGE FILE

With the image file ready, we continue with global variables: arrays "original_bg_pixels", "bg_pixels_graph", and "bg_pixels_stats" for image pixels, dimensions "orig_w" and "orig_h" at zero, current positions and sizes initialized from inputs, booleans like "panel_dragging" false, "resizing" false, enumerations "resize_mode" and "hover_mode" to "NONE", integers for resize starts and mins like "resize_thickness" at five, "min_width" at two hundred, hovers like "header_hovered" false, "prev_mouse_state" at zero, layout constants such as "header_height" at twenty-seven, "gap_y" at seven, "button_size" at twenty-five, offsets for buttons, "panels_minimized" false, colors like "HeaderColor" to medium gray, "HeaderHoverColor" to red, theme flag "is_dark_theme" true, light mode colors such as "LightHeaderColor" to silver, and flags "graphCreated" and "statsCreated" false.

With that done, the next thing we will need to do is create a function to dynamically scale the image so it fits the new dimensions. This will come in handy when we want to resize the background image in the panels when allowed. Here is the logic we used to achieve that.

//+------------------------------------------------------------------+
//| Scale image                                                      |
//+------------------------------------------------------------------+
void ScaleImage(uint &pixels[], int original_width, int original_height, int new_width, int new_height) {
   uint scaled_pixels[];                                         //--- Declare scaled array
   ArrayResize(scaled_pixels, new_width * new_height);           //--- Resize scaled
   for (int y = 0; y < new_height; y++)                          //--- Loop new rows
   {
      for (int x = 0; x < new_width; x++)                        //--- Loop new columns
      {
         double original_x = (double)x * original_width / new_width; //--- Compute original x
         double original_y = (double)y * original_height / new_height; //--- Compute original y
         uint pixel = BicubicInterpolate(pixels, original_width, original_height, original_x, original_y); //--- Interpolate pixel
         scaled_pixels[y * new_width + x] = pixel;               //--- Set scaled pixel
      }
   }
   ArrayResize(pixels, new_width * new_height);                  //--- Resize original
   ArrayCopy(pixels, scaled_pixels);                             //--- Copy scaled
}

//+------------------------------------------------------------------+
//| Bicubic interpolate pixel                                        |
//+------------------------------------------------------------------+
uint BicubicInterpolate(uint &pixels[], int width, int height, double x, double y) {
   int x0 = (int)x;                                              //--- Get integer x
   int y0 = (int)y;                                              //--- Get integer y
   double fractional_x = x - x0;                                 //--- Get fractional x
   double fractional_y = y - y0;                                 //--- Get fractional y
   int x_indices[4], y_indices[4];                               //--- Declare indices
   for (int i = -1; i <= 2; i++)                                 //--- Loop offsets
   {
      x_indices[i + 1] = MathMin(MathMax(x0 + i, 0), width - 1); //--- Clamp x index
      y_indices[i + 1] = MathMin(MathMax(y0 + i, 0), height - 1); //--- Clamp y index
   }
   uint neighborhood_pixels[16];                                 //--- Declare neighborhood
   for (int j = 0; j < 4; j++)                                   //--- Loop y indices
   {
      for (int i = 0; i < 4; i++)                                //--- Loop x indices
      {
         neighborhood_pixels[j * 4 + i] = pixels[y_indices[j] * width + x_indices[i]]; //--- Get pixel
      }
   }
   uchar alpha_components[16], red_components[16], green_components[16], blue_components[16]; //--- Declare components
   for (int i = 0; i < 16; i++)                                  //--- Loop pixels
   {
      GetArgb(neighborhood_pixels[i], alpha_components[i], red_components[i], green_components[i], blue_components[i]); //--- Get ARGB
   }
   uchar alpha_out = (uchar)BicubicInterpolateComponent(alpha_components, fractional_x, fractional_y); //--- Interpolate alpha
   uchar red_out = (uchar)BicubicInterpolateComponent(red_components, fractional_x, fractional_y); //--- Interpolate red
   uchar green_out = (uchar)BicubicInterpolateComponent(green_components, fractional_x, fractional_y); //--- Interpolate green
   uchar blue_out = (uchar)BicubicInterpolateComponent(blue_components, fractional_x, fractional_y); //--- Interpolate blue
   return (((uint)alpha_out) << 24) | (((uint)red_out) << 16) | (((uint)green_out) << 8) | ((uint)blue_out); //--- Return interpolated
}

//+------------------------------------------------------------------+
//| Bicubic interpolate component                                    |
//+------------------------------------------------------------------+
double BicubicInterpolateComponent(uchar &components[], double fractional_x, double fractional_y) {
   double weights_x[4];                                          //--- Declare x weights
   double t = fractional_x;                                      //--- Set t x
   weights_x[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_x[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_x[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_x[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double y_values[4];                                           //--- Declare y values
   for (int j = 0; j < 4; j++)                                   //--- Loop y
   {
      y_values[j] = weights_x[0] * components[j * 4 + 0] + weights_x[1] * components[j * 4 + 1] + weights_x[2] * components[j * 4 + 2] + weights_x[3] * components[j * 4 + 3]; //--- Compute y value
   }
   double weights_y[4];                                          //--- Declare y weights
   t = fractional_y;                                             //--- Set t y
   weights_y[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_y[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_y[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_y[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double result = weights_y[0] * y_values[0] + weights_y[1] * y_values[1] + weights_y[2] * y_values[2] + weights_y[3] * y_values[3]; //--- Compute result
   return MathMax(0, MathMin(255, result));                      //--- Clamp result
}

//+------------------------------------------------------------------+
//| Get ARGB components                                              |
//+------------------------------------------------------------------+
void GetArgb(uint pixel, uchar &alpha, uchar &red, uchar &green, uchar &blue) {
   alpha = (uchar)((pixel >> 24) & 0xFF);                        //--- Get alpha
   red = (uchar)((pixel >> 16) & 0xFF);                          //--- Get red
   green = (uchar)((pixel >> 8) & 0xFF);                         //--- Get green
   blue = (uchar)(pixel & 0xFF);                                 //--- Get blue
}

First, we implement the "ScaleImage" function to resize an image array of pixels from original dimensions to new width and height, using bicubic interpolation for smooth scaling. It takes a reference to the pixels array, original width and height, and new dimensions. We declare and resize a temporary "scaled_pixels" array to the new size, then nest loops over new height and width to compute corresponding original coordinates by proportional mapping. For each new pixel, we call "BicubicInterpolate" with the original array and fractional coordinates to get the interpolated value and store it in "scaled_pixels". Finally, we resize the input pixels array to the new size and copy "scaled_pixels" into it with the ArrayCopy function.

Next is the "BicubicInterpolate" function to compute a single pixel value using bicubic interpolation at given fractional x and y in the image. It takes the pixels array, width, height, and doubles x and y. We get integer parts x0 and y0, fractional parts, and create index arrays for a 4x4 neighborhood by clamping offsets from -1 to 2 to image bounds with the MathMin and MathMax functions. We extract sixteen neighborhood pixels into an array, then declare component arrays for alpha, red, green, and blue, looping to populate them using "GetArgb". We interpolate each component with "BicubicInterpolateComponent" and the fractions, casting to uchar, and combine into a uint ARGB value with bit shifts.

We then create the "BicubicInterpolateComponent" function to perform bicubic interpolation on a single color channel's 4x4 component array using fractional x and y. It computes four x weights with the bicubic kernel formula based on t as fractional x, then calculates four intermediate y values by weighted sums of rows in the components array. Similarly, it computes y weights with fractional y as t, and derives the final result as a weighted sum of those y values, clamping between zero and 255 with "MathMax" and "MathMin". Finally, we implement the "GetArgb" function to extract ARGB components from a uint pixel value into uchar references for alpha, red, green, and blue, using bit shifts right by 24/16/8/0 and masking with 0xFF.

It is important to understand that it is not a must that you use a bicubic approach. You can use a linear or bilinear approach, but that will give a more jagged image than what we actually want. So we go with the best approach for an antialiased image pixelation. In fact, have a look below at the different approaches you could have.

DIFFERENT INTERPOLATION APPROACHES

We can see that the bicubic interpolation approach gives the smoothest image view compared to the other approaches. The next thing that we will do is use the functions to resize our resource image so it fits dynamically. Our image file is quite larger than our canvas area where we want to render it, so we need to resize it. Let's do that in the initialization event handler.

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()
{
   currentWidth = CanvasWidth;                                   //--- Set initial width
   currentHeight = CanvasHeight;                                 //--- Set initial height
   currentCanvasX = CanvasX;                                     //--- Set initial X
   currentCanvasY = CanvasY;                                     //--- Set initial Y
   
   if (UseBackground)                                            //--- Check if background enabled
   {
      if (ResourceReadImage("::1. Transparent MT5 bmp image.bmp", original_bg_pixels, orig_w, orig_h) && orig_w > 0 && orig_h > 0) //--- Load image if valid
      {
         ArrayCopy(bg_pixels_graph, original_bg_pixels);         //--- Copy to graph background
         ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph background
         if (EnableStatsPanel)                                   //--- Check stats panel
         {
            ArrayCopy(bg_pixels_stats, original_bg_pixels);      //--- Copy to stats background
            ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats background
         }
      }
      else                                                       //--- Handle load failure
      {
         Print("Failed to load background image from ::1. Transparent MT5 bmp image.bmp"); //--- Print error
      }
   }
   
   int header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute header width
   if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX, currentCanvasY, header_width, header_height, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create header canvas
   {
      Print("Failed to create Header Canvas");                   //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   
   if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create graph canvas
   {
      Print("Failed to create Graph Canvas");                    //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   graphCreated = true;                                          //--- Set graph created
   
   if (EnableStatsPanel)                                         //--- Check stats panel
   {
      int statsX = currentCanvasX + currentWidth + PanelGap;     //--- Compute stats X
      if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create stats canvas
      {
         Print("Failed to create Stats Canvas");                 //--- Print error
      }
      statsCreated = true;                                       //--- Set stats created
   }
   
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

In the OnInit event handler, we set up the initial state and create the canvas panels for the dashboard. We initialize current dimensions and positions from inputs like "currentWidth" to "CanvasWidth", "currentHeight" to "CanvasHeight", "currentCanvasX" to "CanvasX", and "currentCanvasY" to "CanvasY". If "UseBackground" is true, we load the resource image with ResourceReadImage into "original_bg_pixels" and get its original width and height. If successful, we copy to "bg_pixels_graph" and scale it to the current dimensions using "ScaleImage". For the stats panel, if "EnableStatsPanel" is true, we copy to "bg_pixels_stats" and scale to half the width with full height. On failure, we print an error message.

We compute header width as graph width plus optional stats width and gap, then create the header canvas with "CreateBitmapLabel" at subwindow zero, name "canvasHeaderName", position, width, and "header_height" using COLOR_FORMAT_ARGB_NORMALIZE, printing an error and returning "INIT_FAILED" if unsuccessful. Similarly, we create the graph canvas below the header plus "gap_y", setting "graphCreated" to true on success or returning INIT_FAILED on failure. If stats enabled, we compute its x position after the graph plus "PanelGap", create the stats canvas, and set "statsCreated" to true. Finally, we redraw the chart with "ChartRedraw" and return INIT_SUCCEEDED. This creates the paper areas, and no actual drawing yet. To demonstrate this, here is a sample of what we can see on the tooltips upon compilation.

CANVAS AREAS

We can see that the canvas area papers are now drawn. What we need to do is do the actual drawing to render the canvas objects. We will start with the header, but since we need to be theme sensitive, we will first define some theme helper functions to enable us create either a dark or light theme render where applicable.

//+------------------------------------------------------------------+
//| Get theme-aware colors                                           |
//+------------------------------------------------------------------+
color GetHeaderColor() { return is_dark_theme ? HeaderColor : LightHeaderColor; }                //--- Return header color
color GetHeaderHoverColor() { return is_dark_theme ? HeaderHoverColor : LightHeaderHoverColor; } //--- Return hover color
color GetHeaderDragColor() { return is_dark_theme ? HeaderDragColor : LightHeaderDragColor; }    //--- Return drag color
color GetStatsLabelColor() { return is_dark_theme ? StatsLabelColor : LightStatsLabelColor; }    //--- Return label color
color GetStatsValueColor() { return is_dark_theme ? StatsValueColor : LightStatsValueColor; }    //--- Return value color
color GetStatsHeaderColor() { return is_dark_theme ? StatsHeaderColor : LightStatsHeaderColor; } //--- Return header color
color GetBorderColor() { return is_dark_theme ? borderColor : LightBorderColor; }                //--- Return border color
color GetTopColor() { return is_dark_theme ? TopColor : LightTopColor; }                         //--- Return top color
color GetBottomColor() { return is_dark_theme ? BottomColor : LightBottomColor; }                //--- Return bottom color
color GetHeaderTextColor() { return is_dark_theme ? clrWhite : LightHeaderTextColor; }           //--- Return text color
color GetIconColor(bool is_drag) { return is_drag ? GetHeaderDragColor() : GetHeaderHoverColor(); } //--- Return icon color

Here, we implement several getter functions to retrieve theme-aware colors based on the current "is_dark_theme" flag, ensuring consistent visuals across dark and light modes without redundant checks elsewhere. The "GetHeaderColor" function returns "HeaderColor" in dark mode or "LightHeaderColor" in light mode for the header background. We use the same logic for all the other functions. We can now use these helper functions to create the header canvas objects.

//+------------------------------------------------------------------+
//| Draw header on header canvas                                     |
//+------------------------------------------------------------------+
void DrawHeaderOnCanvas()
{
   canvasHeader.Erase(0);                                        //--- Clear canvas
   
   color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Set background
   uint argb_bg = ColorToARGB(header_bg, 255);                   //--- Convert to ARGB
   canvasHeader.FillRectangle(0, 0, canvasHeader.Width() - 1, header_height - 1, argb_bg); //--- Fill background
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasHeader.Line(0, 0, canvasHeader.Width() - 1, 0, argbBorder); //--- Draw top border
   canvasHeader.Line(canvasHeader.Width() - 1, 0, canvasHeader.Width() - 1, header_height - 1, argbBorder); //--- Draw right border
   canvasHeader.Line(canvasHeader.Width() - 1, header_height - 1, 0, header_height - 1, argbBorder); //--- Draw bottom border
   canvasHeader.Line(0, header_height - 1, 0, 0, argbBorder);    //--- Draw left border
   
   canvasHeader.FontSet("Arial Bold", 15);                       //--- Set font
   uint argbText = ColorToARGB(GetHeaderTextColor(), 255);       //--- Convert text to ARGB
   canvasHeader.TextOut(10, (header_height - 15) / 2, "Price Dashboard", argbText, TA_LEFT); //--- Draw title
   
   int theme_x = canvasHeader.Width() + theme_x_offset;          //--- Compute theme x
   string theme_symbol = CharToString((uchar)91);                //--- Set theme symbol
   color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Set theme color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_theme = ColorToARGB(theme_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(theme_x, (header_height - 22) / 2, theme_symbol, argb_theme, TA_CENTER); //--- Draw theme icon
   
   int min_x = canvasHeader.Width() + minimize_x_offset;         //--- Compute minimize x
   string min_symbol = panels_minimized ? CharToString((uchar)111) : CharToString((uchar)114); //--- Set minimize symbol
   color min_color = minimize_hovered ? clrYellow : GetHeaderTextColor(); //--- Set minimize color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_min = ColorToARGB(min_color, 255);                  //--- Convert to ARGB
   canvasHeader.TextOut(min_x, (header_height - 22) / 2, min_symbol, argb_min, TA_CENTER); //--- Draw minimize icon
   
   int close_x = canvasHeader.Width() + close_x_offset;          //--- Compute close x
   string close_symbol = CharToString((uchar)114);               //--- Set close symbol
   color close_color = close_hovered ? clrRed : GetHeaderTextColor(); //--- Set close color
   canvasHeader.FontSet("Webdings", 22);                         //--- Set font
   uint argb_close = ColorToARGB(close_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(close_x, (header_height - 22) / 2, close_symbol, argb_close, TA_CENTER); //--- Draw close icon
   
   canvasHeader.Update();                                        //--- Update canvas
}

Here, we implement the "DrawHeaderOnCanvas" function to render the header section on the "canvasHeader" object, providing a title and interactive icons with dynamic colors based on states like dragging or hovering. We start by clearing the canvas with the "Erase" method, set to zero. We determine the background color conditionally: if "panel_dragging" is true, use "GetHeaderDragColor"; else if "header_hovered", use "GetHeaderHoverColor"; otherwise, "GetHeaderColor". We convert it to ARGB with ColorToARGB at full opacity 255 and fill a rectangle from (0,0) to width minus one and "header_height" minus one using the FillRectangle method.

For borders, we convert the border color from "GetBorderColor" to ARGB at 255 and draw lines with Line for the top, right, bottom, and left edges of the header. We set the font to Arial Bold at size fifteen with FontSet, convert header text color from "GetHeaderTextColor" to ARGB, and draw the title 'Price Dashboard' at x ten centered vertically using TextOut with left alignment.

For the theme icon, we compute its x as canvas width plus "theme_x_offset", set symbol to character 91 from uchar cast, color to yellow if "theme_hovered" else header text color, change font to Wingdings at twenty-two, convert to ARGB, and draw centered horizontally and vertically with "TextOut" and center alignment. Similarly, for the minimize icon, compute x with "minimize_x_offset", set symbol conditionally to character 111 if "panels_minimized" or 114 otherwise, color to yellow on hover, else text color, use Wingdings font, convert, and draw centered. For the close icon, compute x with "close_x_offset", set symbol to 114, color to red on "close_hovered" else text color, switch font to Webdings at twenty-two, convert, and draw centered. Finally, we update the canvas with "Update" to display the changes. When we call this function in initialization, we get the following outcome.

CANVAS HEADER RENDER

From the image, we can see that the canvas header is correctly labelled. What we need to do next is draw on the graph canvas area, where we are supposed to do the analysis of the prices and draw their line graph. So essentially, we are drawing our own simple price graph. However, note that this is just our approach; it is the easiest we thought for this demonstration. You can use your own complex calculations as you deem fit. Here is the approach we use to get that done.

//+------------------------------------------------------------------+
//| Update the price graph on the main Canvas                        |
//+------------------------------------------------------------------+
void UpdateGraphOnCanvas()
{
   canvasGraph.Erase(0);                                         //--- Clear canvas
   
   if (UseBackground && ArraySize(bg_pixels_graph) == currentWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < currentWidth; x++)                  //--- Loop columns
         {
            canvasGraph.PixelSet(x, y, bg_pixels_graph[y * currentWidth + x]); //--- Set pixel
         }
      }
   }
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasGraph.Line(0, 0, currentWidth - 1, 0, argbBorder);      //--- Draw top outer
   canvasGraph.Line(currentWidth - 1, 0, currentWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
   canvasGraph.Line(currentWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
   canvasGraph.Line(0, currentHeight - 1, 0, 0, argbBorder);     //--- Draw left outer
   canvasGraph.Line(1, 1, currentWidth - 2, 1, argbBorder);      //--- Draw top inner
   canvasGraph.Line(currentWidth - 2, 1, currentWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
   canvasGraph.Line(currentWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
   canvasGraph.Line(1, currentHeight - 2, 1, 1, argbBorder);     //--- Draw left inner
   
   double closePrices[];                                         //--- Declare close array
   ArrayResize(closePrices, graphBars);                          //--- Resize close
   if (CopyClose(_Symbol, _Period, 0, graphBars, closePrices) != graphBars) //--- Copy closes
   {
      Print("Failed to copy close prices");                      //--- Print error
      return;                                                    //--- Exit
   }
   
   datetime timeArr[];                                           //--- Declare time array
   ArrayResize(timeArr, graphBars);                              //--- Resize time
   if (CopyTime(_Symbol, _Period, 0, graphBars, timeArr) != graphBars) //--- Copy times
   {
      Print("Failed to copy times");                             //--- Print error
      return;                                                    //--- Exit
   }
   
   double minPrice = closePrices[0];                             //--- Set initial min
   double maxPrice = closePrices[0];                             //--- Set initial max
   for (int i = 1; i < graphBars; i++)                           //--- Loop prices
   {
      if (closePrices[i] < minPrice) minPrice = closePrices[i];  //--- Update min
      if (closePrices[i] > maxPrice) maxPrice = closePrices[i];  //--- Update max
   }
   double priceRange = maxPrice - minPrice;                      //--- Compute range
   if (priceRange == 0) priceRange = _Point;                     //--- Avoid zero
   
   int graphLeft = 2;                                            //--- Set left margin
   int graphRight = currentWidth - 3;                            //--- Set right margin
   double graphWidth_d = graphRight - graphLeft;                 //--- Compute width
   int graphHeight = currentHeight - 4;                          //--- Compute height
   int bottomY = 2 + graphHeight;                                //--- Set bottom y
   
   int x_pos[];                                                  //--- Declare x positions
   int y_pos[];                                                  //--- Declare y positions
   ArrayResize(x_pos, graphBars);                                //--- Resize x
   ArrayResize(y_pos, graphBars);                                //--- Resize y
   for (int i = 0; i < graphBars; i++)                           //--- Loop bars
   {
      double norm = (graphBars > 1) ? (double)i / (graphBars - 1) : 0.0; //--- Normalize
      x_pos[i] = graphLeft + (int)(norm * graphWidth_d + 0.5);   //--- Set x
      double price = closePrices[graphBars - 1 - i];             //--- Get price (flipped)
      y_pos[i] = 2 + (int)(graphHeight * (maxPrice - price) / priceRange + 0.5); //--- Set y
   }
   
   color lineColor = clrBlue;                                    //--- Set line color
   uint argbLine = ColorToARGB(lineColor, 255);                  //--- Convert to ARGB
   for (int i = 0; i < graphBars - 1; i++)                       //--- Loop segments
   {
      int x1 = (currentWidth - 1) - x_pos[i];                    //--- Set x1 (flipped)
      int y1 = y_pos[i];                                         //--- Set y1
      int x2 = (currentWidth - 1) - x_pos[i + 1];                //--- Set x2 (flipped)
      int y2 = y_pos[i + 1];                                     //--- Set y2
      canvasGraph.LineAA(x1, y1, x2, y2, argbLine);              //--- Draw line
   }
   
   int min_flipped_x = (currentWidth - 1) - graphRight;          //--- Set min flipped x
   int max_flipped_x = (currentWidth - 1) - graphLeft;           //--- Set max flipped x
   for (int colX = min_flipped_x; colX <= max_flipped_x; colX++) //--- Loop columns
   {
      int logical_colX = (currentWidth - 1) - colX;              //--- Get logical x
      int seg = -1;                                              //--- Initialize segment
      for (int j = 0; j < graphBars - 1; j++)                    //--- Loop segments
      {
         if (x_pos[j] <= logical_colX && logical_colX <= x_pos[j + 1]) //--- Check segment
         {
            seg = j;                                             //--- Set segment
            break;                                               //--- Exit
         }
      }
      if (seg == -1) continue;                                   //--- Skip if no segment
      
      double dx = x_pos[seg + 1] - x_pos[seg];                   //--- Compute dx
      double t = (dx > 0) ? (logical_colX - x_pos[seg]) / dx : 0.0; //--- Compute t
      double interpY = y_pos[seg] + t * (y_pos[seg + 1] - y_pos[seg]); //--- Interpolate y
      int topY = (int)(interpY + 0.5);                           //--- Round top y
      
      for (int fillY = topY; fillY < bottomY; fillY++)           //--- Loop fill
      {
         double fadeFactor = (double)(bottomY - fillY) / (bottomY - topY); //--- Compute fade
         uchar alpha = (uchar)(255 * fadeFactor * FogOpacity);   //--- Compute alpha
         uint argbFill = ColorToARGB(lineColor, alpha);          //--- Convert fill
         if (BlendFog)                                           //--- Check blend
         {
            uint currentPixel = canvasGraph.PixelGet(colX, fillY); //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels
            canvasGraph.PixelSet(colX, fillY, blendedPixel);     //--- Set blended
         }
         else                                                    //--- Handle no blend
         {
            canvasGraph.PixelSet(colX, fillY, argbFill);         //--- Set fill
         }
      }
   }
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   uint argbText = ColorToARGB(is_dark_theme ? clrBlack : clrGray, 255); //--- Convert text
   canvasGraph.TextOut(currentWidth / 2, 10, "Price Graph (" + _Symbol + ")", argbText, TA_CENTER); //--- Draw title
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   
   string newTime = TimeToString(timeArr[0], TIME_DATE | TIME_MINUTES); //--- Get new time
   string oldTime = TimeToString(timeArr[graphBars - 1], TIME_DATE | TIME_MINUTES); //--- Get old time
   canvasGraph.TextOut(10, currentHeight - 15, newTime, argbText, TA_LEFT); //--- Draw new time
   canvasGraph.TextOut(currentWidth - 10, currentHeight - 15, oldTime, argbText, TA_RIGHT); //--- Draw old time
   
   if (resize_hovered || resizing)                               //--- Check resize state
   {
      ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get active mode
      if (active_mode == NONE)                                       //--- Check none
      {
         canvasGraph.Update();                                   //--- Update canvas
         return;                                                 //--- Exit
      }
      
      string icon_font = "Wingdings 3";                          //--- Set icon font
      int icon_size = 25;                                        //--- Set icon size
      uchar icon_code;                                           //--- Declare code
      int angle = 0;                                             //--- Set angle
      switch (active_mode)                                       //--- Switch mode
      {
         case BOTTOM: 
            icon_code = (uchar)'2';                              //--- Set bottom code
            angle = 0;                                           //--- Set angle
            break;
         case RIGHT: 
            icon_code = (uchar)'1';                              //--- Set right code
            angle = 0;                                           //--- Set angle
            break;
         case BOTTOM_RIGHT: 
            icon_code = (uchar)'2';                              //--- Set corner code
            angle = 450;                                         //--- Set angle
            break;
         default: canvasGraph.Update(); return;
      }
      string icon_symbol = CharToString(icon_code);              //--- Set symbol
      
      color icon_color = GetIconColor(resizing);                 //--- Get icon color
      uint argb_icon = ColorToARGB(icon_color, 255);             //--- Convert to ARGB
      
      canvasGraph.FontSet(icon_font, icon_size);                 //--- Set font
      canvasGraph.FontAngleSet(angle);                           //--- Set angle
      
      int icon_x = 0;                                            //--- Initialize x
      int icon_y = 0;                                            //--- Initialize y
      
      switch (active_mode)                                       //--- Switch for position
      {
         case BOTTOM:
            icon_x = MathMax(0, MathMin(hover_mouse_local_x - (icon_size / 2), currentWidth - icon_size)); //--- Set x
            icon_y = currentHeight - icon_size - 2;              //--- Set y
            break;
         case RIGHT:
            icon_y = MathMax(0, MathMin(hover_mouse_local_y - (icon_size / 2), currentHeight - icon_size)); //--- Set y
            icon_x = currentWidth - icon_size - 2;               //--- Set x
            break;
         case BOTTOM_RIGHT:
            icon_x = currentWidth - icon_size - 10;              //--- Set x
            icon_y = currentHeight - icon_size;                  //--- Set y
            break;
         default: break;
      }
      
      canvasGraph.TextOut(icon_x, icon_y, icon_symbol, argb_icon, TA_LEFT | TA_TOP); //--- Draw icon
      canvasGraph.FontAngleSet(0);                               //--- Reset angle
   }
   
   canvasGraph.Update();                                         //--- Update canvas
}

Here, we implement the "UpdateGraphOnCanvas" function to render the price graph on the "canvasGraph" object, displaying recent bar closes as a line plot with filled areas, labels, and optional resize indicators. We begin by clearing the canvas with Erase set to zero. If "UseBackground" is true and "bg_pixels_graph" matches the current dimensions, we loop over height and width to set each pixel from the scaled background array using the PixelSet method. We convert the border color from "GetBorderColor" to ARGB at full opacity with ColorToARGB and draw outer and inner borders using "Line" for top, right, bottom, and left edges, creating a double border effect.

We declare and resize a "closePrices" double array to "graphBars", copy closing prices with CopyClose for the current symbol and period starting from bar zero, printing an error and exiting if incomplete. Similarly, we fetch times into a "timeArr" datetime array with CopyTime, handling failure. We find the minimum and maximum prices by initializing to the first close and looping to update, compute the range, defaulting to _Point if zero to avoid division issues.

We set margins like "graphLeft" at two, "graphRight" at width minus three, compute effective width and height, and bottom y at two plus height. We resize integer arrays "x_pos" and "y_pos" to "graphBars", then loop to normalize positions: x as left plus normalized fraction of width rounded, y as two plus scaled (max minus price) over range rounded, flipping the price array index for recent on left. We set line color to blue, convert to ARGB, and loop over segments to draw anti-aliased lines with LineAA, flipping x positions for right-to-left orientation.

For filling, we compute min and max flipped x, loop over columns from min to max flipped (right to left), derive logical x, find the segment containing it by checking positions, and skip if none. We calculate the interpolation factor t, interpolate y, and round to the top y. Then, loop from top y to bottom y, compute fade factor from bottom, alpha as 255 times fade times "FogOpacity", convert fill to ARGB with that alpha. If "BlendFog" is true, get the current pixel with PixelGet, blend with "BlendPixels", and set; else, set directly with "PixelSet". For blending the pixels, we use a custom helper function whose code snippet is as follows.

//+------------------------------------------------------------------+
//| Alpha blending function for two ARGB colors                      |
//+------------------------------------------------------------------+
uint BlendPixels(uint bg, uint fg)
{
   uchar bgA = (uchar)((bg >> 24) & 0xFF);                       //--- Get bg alpha
   uchar bgR = (uchar)((bg >> 16) & 0xFF);                       //--- Get bg red
   uchar bgG = (uchar)((bg >> 8) & 0xFF);                        //--- Get bg green
   uchar bgB = (uchar)(bg & 0xFF);                               //--- Get bg blue
   
   uchar fgA = (uchar)((fg >> 24) & 0xFF);                       //--- Get fg alpha
   uchar fgR = (uchar)((fg >> 16) & 0xFF);                       //--- Get fg red
   uchar fgG = (uchar)((fg >> 8) & 0xFF);                        //--- Get fg green
   uchar fgB = (uchar)(fg & 0xFF);                               //--- Get fg blue
   
   if (fgA == 0) return bg;                                      //--- Return bg if transparent
   if (fgA == 255) return fg;                                    //--- Return fg if opaque
   
   double alphaFg = fgA / 255.0;                                 //--- Compute fg alpha
   double alphaBg = 1.0 - alphaFg;                               //--- Compute bg alpha
   
   uchar outR = (uchar)(fgR * alphaFg + bgR * alphaBg);          //--- Blend red
   uchar outG = (uchar)(fgG * alphaFg + bgG * alphaBg);          //--- Blend green
   uchar outB = (uchar)(fgB * alphaFg + bgB * alphaBg);          //--- Blend blue
   uchar outA = (uchar)(fgA + bgA * alphaBg);                    //--- Blend alpha
   
   return (((uint)outA) << 24) | (((uint)outR) << 16) | (((uint)outG) << 8) | ((uint)outB); //--- Return blended
}

For the function, we use a similar approach as we did with the ARGB function, using bitwise operations. We have added comments for clarity. Continuing, we set the font to Arial at twelve, convert text color themed (black in dark, gray in light) to ARGB, and draw a centered title 'Price Graph' with a symbol at the top. We format the newest and oldest times with TimeToString using date and minutes, draw left-aligned at the bottom left and right-aligned at the bottom right.

If "resize_hovered" or "resizing" is true, we get active mode from "resize_mode" or "hover_mode", and exit early if none. We set the icon font to Wingdings 3 at twenty-five, determine code and angle based on mode (bottom/right as '2'/'1' at zero, corner '2' at 450), and convert the symbol with the CharToString function. As for the icons, you can choose the best that fit your style. We chose those since MQL5 does not yet have in-built cursor changes, so we had to be creative. Here is a visualization of the font symbols you can use.

SYMBOL FONTS

Next, we get icon color from "GetIconColor" with resizing flag, convert to ARGB, set font and angle with FontSet and FontAngleSet, compute position based on mode using "MathMax"/"MathMin" for clamping and hover local coordinates or fixed offsets, draw with TextOut left top aligned, and reset angle to zero. Finally, we update the canvas with Update to show the graph. When we call the function in the initialization event handler, we get the following outcome.

GRAPH CANVAS

With the graph canvas rendered, we now need to create the other statistics panel on the right of the graph canvas. For this one, we want to advance a bit and mix two colors for the background where they meet using linear interpolation, since it is just a simple thing we want to have, and also darken the border colors based on the background selected colors, instead of the static border colors that we have been having for the header and the graph canvas so far. To achieve that with ease, we will need some helper functions.

//+------------------------------------------------------------------+
//| Linear interpolation between two colors                          |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   uchar r1 = (uchar)((start >> 16) & 0xFF);                     //--- Get start red
   uchar g1 = (uchar)((start >> 8) & 0xFF);                      //--- Get start green
   uchar b1 = (uchar)(start & 0xFF);                             //--- Get start blue
   
   uchar r2 = (uchar)((end >> 16) & 0xFF);                       //--- Get end red
   uchar g2 = (uchar)((end >> 8) & 0xFF);                        //--- Get end green
   uchar b2 = (uchar)(end & 0xFF);                               //--- Get end blue
   
   uchar r = (uchar)(r1 + factor * (r2 - r1));                   //--- Interpolate red
   uchar g = (uchar)(g1 + factor * (g2 - g1));                   //--- Interpolate green
   uchar b = (uchar)(b1 + factor * (b2 - b1));                   //--- Interpolate blue
   
   return (r << 16) | (g << 8) | b;                              //--- Return color
}

//+------------------------------------------------------------------+
//| Darken a color by a factor (0.0 to 1.0)                         |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor)
{
   int blue = int((colorValue & 0xFF) * factor);                 //--- Darken blue
   int green = int(((colorValue >> 8) & 0xFF) * factor);         //--- Darken green
   int red = int(((colorValue >> 16) & 0xFF) * factor);          //--- Darken red
   return (color)(blue | (green << 8) | (red << 16));            //--- Return darkened
}

First, we implement the "InterpolateColor" function to linearly blend between two colors based on a factor from zero to one, returning the interpolated color for effects like gradients. It takes color parameters start and end, and a double factor. We extract red, green, and blue components from the start using bit shifts right by sixteen/eight/zero and masking with 0xFF cast to uchar, similarly for the end, just like we did with the other color-based functions. We interpolate each channel as a uchar of start value plus factor times the difference, then combine with red shifted left sixteen, green eight, and blue zero using bit shifts and OR.

Next, we create the "DarkenColor" function to reduce a color's brightness by a factor from zero to one, where one keeps it unchanged, and lower values darken it, returning the adjusted color. It takes color "colorValue" and a double factor, darkens blue as int of blue times factor from mask 0xFF, green from shift eight mask, red from shift sixteen mask, and returns combined with blue OR green shifted eight OR red shifted sixteen cast to color. We can now use these functions in the creation of the statistics panel.

//+------------------------------------------------------------------+
//| Update the stats on the second Canvas                            |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas()
{
   canvasStats.Erase(0);                                         //--- Clear canvas
   
   int statsWidth = currentWidth / 2;                            //--- Compute width
   if (UseBackground && ArraySize(bg_pixels_stats) == statsWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            canvasStats.PixelSet(x, y, bg_pixels_stats[y * statsWidth + x]); //--- Set pixel
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         double factor = (double)y / (currentHeight - 1);        //--- Compute factor
         color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color
         uchar alpha = (uchar)(255 * BackgroundOpacity);         //--- Compute alpha
         uint argbFill = ColorToARGB(currentColor, alpha);       //--- Convert fill
         
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            uint currentPixel = canvasStats.PixelGet(x, y);      //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend
            canvasStats.PixelSet(x, y, blendedPixel);            //--- Set blended
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode for borders
   {
      double reduction = BorderOpacityPercentReduction / 100.0;  //--- Compute reduction
      double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity
      uchar alpha = (uchar)(255 * opacity);                      //--- Set alpha
      double darkenReduction = BorderDarkenPercent / 100.0;      //--- Compute darken
      double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Set factor
      
      for (int y = 0; y < currentHeight; y++)                    //--- Loop vertical
      {
         double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (currentHeight - 1); //--- Get factor
         color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base
         color darkColor = DarkenColor(baseColor, darkenFactor); //--- Darken color
         uint argb = ColorToARGB(darkColor, alpha);              //--- Convert to ARGB
         
         canvasStats.PixelSet(0, y, argb);                       //--- Set left outer
         canvasStats.PixelSet(1, y, argb);                       //--- Set left inner
         
         canvasStats.PixelSet(statsWidth - 1, y, argb);          //--- Set right outer
         canvasStats.PixelSet(statsWidth - 2, y, argb);          //--- Set right inner
      }
      
      double factorTop = 0.0;                                    //--- Set top factor
      color baseTop = GetTopColor();                             //--- Get top base
      color darkTop = DarkenColor(baseTop, darkenFactor);        //--- Darken top
      uint argbTop = ColorToARGB(darkTop, alpha);                //--- Convert top
      for (int x = 0; x < statsWidth; x++)                       //--- Loop top
      {
         canvasStats.PixelSet(x, 0, argbTop);                    //--- Set top outer
         canvasStats.PixelSet(x, 1, argbTop);                    //--- Set top inner
      }
      
      double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor
      color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base
      color darkBot = DarkenColor(baseBot, darkenFactor);        //--- Darken bottom
      uint argbBot = ColorToARGB(darkBot, alpha);                //--- Convert bottom
      for (int x = 0; x < statsWidth; x++)                       //--- Loop bottom
      {
         canvasStats.PixelSet(x, currentHeight - 1, argbBot);    //--- Set bottom outer
         canvasStats.PixelSet(x, currentHeight - 2, argbBot);    //--- Set bottom inner
      }
   }
   else                                                          //--- Handle no color
   {
      uint argbBorder = ColorToARGB(GetBorderColor(), 255);      //--- Convert border
      canvasStats.Line(0, 0, statsWidth - 1, 0, argbBorder);     //--- Draw top outer
      canvasStats.Line(statsWidth - 1, 0, statsWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
      canvasStats.Line(statsWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
      canvasStats.Line(0, currentHeight - 1, 0, 0, argbBorder);  //--- Draw left outer
      canvasStats.Line(1, 1, statsWidth - 2, 1, argbBorder);     //--- Draw top inner
      canvasStats.Line(statsWidth - 2, 1, statsWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
      canvasStats.Line(statsWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
      canvasStats.Line(1, currentHeight - 2, 1, 1, argbBorder);  //--- Draw left inner
   }
   
   color labelColor = GetStatsLabelColor();                      //--- Get label color
   color valueColor = GetStatsValueColor();                      //--- Get value color
   color headerColor = GetStatsHeaderColor();                    //--- Get header color
   
   int yPos = 20;                                                //--- Set initial y
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   uint argbHeader = ColorToARGB(headerColor, 255);              //--- Convert header
   canvasStats.TextOut(statsWidth / 2, yPos, "Account Stats", argbHeader, TA_CENTER); //--- Draw account header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   uint argbLabel = ColorToARGB(labelColor, 255);                //--- Convert label
   uint argbValue = ColorToARGB(valueColor, 255);                //--- Convert value
   
   canvasStats.TextOut(10, yPos, "Name:", argbLabel, TA_LEFT);   //--- Draw name label
   canvasStats.TextOut(statsWidth - 10, yPos, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   canvasStats.TextOut(statsWidth / 2, yPos, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw bar header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   
   double barOpen = iOpen(_Symbol, _Period, 0);                  //--- Get open
   double barHigh = iHigh(_Symbol, _Period, 0);                  //--- Get high
   double barLow = iLow(_Symbol, _Period, 0);                    //--- Get low
   double barClose = iClose(_Symbol, _Period, 0);                //--- Get close
   
   canvasStats.TextOut(10, yPos, "Open:", argbLabel, TA_LEFT);   //--- Draw open label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barOpen, _Digits), argbValue, TA_RIGHT); //--- Draw open value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "High:", argbLabel, TA_LEFT);   //--- Draw high label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barHigh, _Digits), argbValue, TA_RIGHT); //--- Draw high value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Low:", argbLabel, TA_LEFT);    //--- Draw low label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barLow, _Digits), argbValue, TA_RIGHT); //--- Draw low value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Close:", argbLabel, TA_LEFT);  //--- Draw close label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barClose, _Digits), argbValue, TA_RIGHT); //--- Draw close value
   
   canvasStats.Update();                                         //--- Update canvas
}

Here, we implement the "UpdateStatsOnCanvas" function to render the statistics panel on the "canvasStats" object, displaying account and current bar details with themed backgrounds, fills, borders, and text. We clear the canvas with Erase set to zero. If "UseBackground" is true and "bg_pixels_stats" matches the dimensions (half graph width times height), we loop over rows and columns to set each pixel from the scaled background array using the PixelSet method.

If "StatsBackgroundMode" is not "NoColor", we loop over height to compute a vertical factor, determine the row color as "GetTopColor" if single mode or interpolated between top and bottom with "InterpolateColor" if gradient, calculate alpha from "BackgroundOpacity" times 255, convert to ARGB. For each x in the row, get the current pixel with PixelGet, blend it with the fill using "BlendPixels", and set the blended pixel.

For borders in fill modes, we compute reduced opacity from "BorderOpacityPercentReduction" divided by 100.0, clamped to 0.0 to 1.0 times "BackgroundOpacity", alpha as 255 times that, and darken factor as 1.0 minus "BorderDarkenPercent" over 100.0, clamped. Loop over y to get row factor (0.0 if single else normalized), base color as top or interpolated, darken with "DarkenColor", convert to ARGB with alpha, set left outer/inner and right outer/inner pixels. For the top row, use factor 0.0, base top color, darken, ARGB, loop x to set top outer/inner. For bottom, factor 1.0 or 0.0 if single, base bottom or top, darken, ARGB, set bottom outer/inner. If no fill, convert border from "GetBorderColor" to ARGB at 255, draw outer and inner horizontal/vertical lines with "Line" for top/right/bottom/left.

We retrieve themed colors for labels, values, and headers using getters. Set initial y at twenty, font to Arial Bold at "StatsHeaderFontSize", convert header to ARGB, draw centered Account Stats with TextOut, increment y by thirty.

Set font to Arial Bold at "StatsFontSize", convert label and value to ARGB. Draw left-aligned Name: at x ten, right-aligned account name from AccountInfoString with ACCOUNT_NAME at width minus ten, increment y twenty. Similarly, for Balance: with AccountInfoDouble "ACCOUNT_BALANCE" to two decimals, Equity: with ACCOUNT_EQUITY. Increment y thirty, set header font, draw centered Current Bar Stats, increment y thirty. Set font back, fetch current bar open/high/low/close with iOpen/"iHigh"/"iLow"/iClose for symbol/period/bar zero. Draw Open: label and value to _Digits decimals right-aligned, increment y twenty; repeat for High:, Low:, Close:. Finally, update the canvas with Update to display the stats. Upon compilation, we get the following outcome.

STATISTICS PANEL

From the image, we can see that we have created the stats panel with the color interpolation. If you don't want the interpolation, you can do hard boundary mixing as follows: just dump the math.

//+------------------------------------------------------------------+
//| Color selection WITHOUT interpolation (hard switch only)         |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   // Clamp factor just to be safe
   if(factor <= 0.0)
      return start;

   if(factor >= 1.0)
      return end;

   // HARD boundary — no mixing at all
   return (factor < 0.5 ? start : end);
}

When you use this approach, you get the following outcome.

HARD BOUNDARY COLOR MIXING RESULT

From the image, we can see that the boundaries do not mix linearly. So it is upon you to choose the approach that fits your style again. With that done, our dashboard rendering is done for the dark theme mode that we selected to be the default. To enable the chart interactions, we will need to switch on the mouse movements during initialization. Now the initialization event handler looks as follows at the end.

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()

   //--- Existing init logic
   
   DrawHeaderOnCanvas();                                         //--- Draw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);             //--- Enable mouse events
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

We just use the ChartSetInteger function to set the mouse move recognition to true. With that done, we can create some helper functions to track the mouse states and make changes when interacting with our dashboard objects.

//+------------------------------------------------------------------+
//| Check if mouse is over header (excluding buttons)                |
//+------------------------------------------------------------------+
bool IsMouseOverHeader(int mouse_x, int mouse_y)
{
   int header_x = currentCanvasX;                                //--- Get header x
   int header_y = currentCanvasY;                                //--- Get header y
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int header_h = header_height;                                 //--- Get height
   
   if (mouse_x < header_x || mouse_x > header_x + header_w || mouse_y < header_y || mouse_y > header_y + header_h) return false; //--- Check outside
   
   int theme_left = header_x + header_w + theme_x_offset - button_size / 2; //--- Compute theme left
   int theme_right = theme_left + button_size;                   //--- Compute theme right
   int theme_top = header_y;                                     //--- Set theme top
   int theme_bottom = theme_top + header_h;                      //--- Compute theme bottom
   if (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom) return false; //--- Check in theme
   
   int min_left = header_x + header_w + minimize_x_offset - button_size / 2; //--- Compute minimize left
   int min_right = min_left + button_size;                       //--- Compute minimize right
   int min_top = header_y;                                       //--- Set minimize top
   int min_bottom = min_top + header_h;                          //--- Compute minimize bottom
   if (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom) return false; //--- Check in minimize
   
   int close_left = header_x + header_w + close_x_offset - button_size / 2; //--- Compute close left
   int close_right = close_left + button_size;                   //--- Compute close right
   int close_top = header_y;                                     //--- Set close top
   int close_bottom = close_top + header_h;                      //--- Compute close bottom
   if (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom) return false; //--- Check in close
   
   return true;                                                  //--- Return in header
}

//+------------------------------------------------------------------+
//| Check if mouse over theme button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverTheme(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int theme_left = currentCanvasX + header_w + theme_x_offset - button_size / 2; //--- Compute left
   int theme_right = theme_left + button_size;                   //--- Compute right
   int theme_top = currentCanvasY;                               //--- Set top
   int theme_bottom = theme_top + header_height;                 //--- Compute bottom
   return (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom); //--- Check in theme
}

//+------------------------------------------------------------------+
//| Check if mouse over minimize button                              |
//+------------------------------------------------------------------+
bool IsMouseOverMinimize(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int min_left = currentCanvasX + header_w + minimize_x_offset - button_size / 2; //--- Compute left
   int min_right = min_left + button_size;                       //--- Compute right
   int min_top = currentCanvasY;                                 //--- Set top
   int min_bottom = min_top + header_height;                     //--- Compute bottom
   return (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom); //--- Check in minimize
}

//+------------------------------------------------------------------+
//| Check if mouse over close button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverClose(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int close_left = currentCanvasX + header_w + close_x_offset - button_size / 2; //--- Compute left
   int close_right = close_left + button_size;                   //--- Compute right
   int close_top = currentCanvasY;                               //--- Set top
   int close_bottom = close_top + header_height;                 //--- Compute bottom
   return (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom); //--- Check in close
}

//+------------------------------------------------------------------+
//| Check if mouse over resize borders                               |
//+------------------------------------------------------------------+
bool IsMouseOverResize(int mx, int my, ENUM_RESIZE_MODE &rmode)
{
   if (panels_minimized) return false;                           //--- Check if minimized
   int graph_x = currentCanvasX;                                 //--- Get graph x
   int graph_y = currentCanvasY + header_height + gap_y;         //--- Get graph y
   int graph_right = graph_x + currentWidth;                     //--- Compute right
   int graph_bottom = graph_y + currentHeight;                   //--- Compute bottom
   bool over_right = (mx >= graph_right - resize_thickness && mx <= graph_right + resize_thickness) && (my >= graph_y && my <= graph_bottom); //--- Check right
   bool over_bottom = (my >= graph_bottom - resize_thickness && my <= graph_bottom + resize_thickness) && (mx >= graph_x && mx <= graph_right); //--- Check bottom
   if (over_bottom && over_right)                                //--- Check corner
   {
      rmode = BOTTOM_RIGHT;                                      //--- Set bottom-right
      return true;                                               //--- Return true
   }
   else if (over_bottom)                                         //--- Check bottom only
   {
      rmode = BOTTOM;                                            //--- Set bottom
      return true;                                               //--- Return true
   }
   else if (over_right)                                          //--- Check right only
   {
      rmode = RIGHT;                                             //--- Set right
      return true;                                               //--- Return true
   }
   return false;                                                 //--- Return false
}

//+------------------------------------------------------------------+
//| Toggle theme                                                     |
//+------------------------------------------------------------------+
void ToggleTheme()
{
   is_dark_theme = !is_dark_theme;                               //--- Switch theme
   Print("Switched to ", (is_dark_theme ? "Dark" : "Light"), " theme"); //--- Print switch
   DrawHeaderOnCanvas();                                         //--- Redraw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Toggle minimize state                                            |
//+------------------------------------------------------------------+
void ToggleMinimize()
{
   panels_minimized = !panels_minimized;                         //--- Toggle minimized
   if (panels_minimized)                                         //--- Handle minimize
   {
      canvasGraph.Destroy();                                     //--- Destroy graph
      graphCreated = false;                                      //--- Reset graph flag
      if (EnableStatsPanel)                                      //--- Check stats
      {
         canvasStats.Destroy();                                  //--- Destroy stats
         statsCreated = false;                                   //--- Reset stats flag
      }
   }
   else                                                          //--- Handle maximize
   {
      if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate graph
      {
         Print("Failed to recreate Graph Canvas");               //--- Print error
      }
      graphCreated = true;                                       //--- Set graph flag
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel)                                      //--- Check stats
      {
         int statsX = currentCanvasX + currentWidth + PanelGap;  //--- Compute stats X
         if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate stats
         {
            Print("Failed to recreate Stats Canvas");            //--- Print error
         }
         statsCreated = true;                                    //--- Set stats flag
         UpdateStatsOnCanvas();                                  //--- Update stats
      }
   }
   
   int new_header_width = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute new width
   canvasHeader.Resize(new_header_width, header_height);         //--- Resize header
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, new_header_width); //--- Update header width
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
   DrawHeaderOnCanvas();                                         //--- Redraw header
   canvasHeader.Update();                                        //--- Update header
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Close the dashboard                                              |
//+------------------------------------------------------------------+
void CloseDashboard()
{
   canvasHeader.Destroy();                                       //--- Destroy header
   canvasGraph.Destroy();                                        //--- Destroy graph
   if (EnableStatsPanel) canvasStats.Destroy();                  //--- Destroy stats
   ChartRedraw();                                                //--- Redraw chart
}

Here, we implement several hover detection functions to identify mouse positions over specific areas like the header (excluding buttons) and individual buttons for theme, minimize, and close, using current positions and sizes to return booleans for state updates. We also add checks for resize borders on the graph panel, determining the mode (bottom, right, or corner) based on thickness and reference to update hover or resize enumerations.

Additionally, we create toggle functions for theme switching by flipping the flag, printing the new mode, and redrawing all canvases, and for minimizing by toggling the state, destroying/recreating graph and stats canvases as needed, resizing the header, redrawing it, and updating. Finally, we define a close function to destroy all canvases and redraw the chart. We define the "IsMouseOverHeader" function to check if the mouse is over the header area without overlapping buttons, returning a boolean. It gets header position from "currentCanvasX" and "currentCanvasY", computes width including optional stats and gap if not minimized, height from "header_height", and returns false if outside bounds.

We then calculate button areas for theme, minimize, and close using offsets and "button_size", returning false if any, else true for header hover. As for the next of the functions, we don't really have to explain it since we already used a similar approach in the prior article parts in this series. We added comments for clarity. The next thing that we will do is use these functions in the chart event handler as follows.

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (id == CHARTEVENT_CHART_CHANGE)                            //--- Check change event
   {
      DrawHeaderOnCanvas();                                      //--- Redraw header
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
   }
   else if (id == CHARTEVENT_MOUSE_MOVE)                         //--- Handle mouse move
   {
      int mouse_x = (int)lparam;                                 //--- Get mouse x
      int mouse_y = (int)dparam;                                 //--- Get mouse y
      int mouse_state = (int)sparam;                             //--- Get mouse state
      
      bool prev_header_hovered = header_hovered;                 //--- Store previous header
      bool prev_min_hovered = minimize_hovered;                  //--- Store previous minimize
      bool prev_close_hovered = close_hovered;                   //--- Store previous close
      bool prev_theme_hovered = theme_hovered;                   //--- Store previous theme
      bool prev_resize_hovered = resize_hovered;                 //--- Store previous resize
      
      header_hovered = IsMouseOverHeader(mouse_x, mouse_y);      //--- Check header hover
      theme_hovered = IsMouseOverTheme(mouse_x, mouse_y);        //--- Check theme hover
      minimize_hovered = IsMouseOverMinimize(mouse_x, mouse_y); //--- Check minimize hover
      close_hovered = IsMouseOverClose(mouse_x, mouse_y);        //--- Check close hover
      resize_hovered = IsMouseOverResize(mouse_x, mouse_y, hover_mode); //--- Check resize hover
      
      if (resize_hovered || resizing)                            //--- Check resize state
      {
         hover_mouse_local_x = mouse_x - currentCanvasX;         //--- Set local x
         hover_mouse_local_y = mouse_y - (currentCanvasY + header_height + gap_y); //--- Set local y
      }
      
      bool hover_changed = (prev_header_hovered != header_hovered || prev_min_hovered != minimize_hovered || 
                            prev_close_hovered != close_hovered || prev_theme_hovered != theme_hovered ||
                            prev_resize_hovered != resize_hovered); //--- Check change
      
      if (hover_changed)                                         //--- If changed
      {
         DrawHeaderOnCanvas();                                   //--- Redraw header
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      else if ((resize_hovered || resizing) && (mouse_x != last_mouse_x || mouse_y != last_mouse_y)) //--- Check position change
      {
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      
      string header_tooltip = "";                                //--- Initialize tooltip
      if (theme_hovered) header_tooltip = "Toggle Theme (Dark/Light)"; //--- Set theme tooltip
      else if (minimize_hovered) header_tooltip = panels_minimized ? "Maximize Panels" : "Minimize Panels"; //--- Set minimize tooltip
      else if (close_hovered) header_tooltip = "Close Dashboard"; //--- Set close tooltip
      ObjectSetString(0, canvasHeaderName, OBJPROP_TOOLTIP, header_tooltip); //--- Set header tooltip
      
      string resize_tooltip = "";                                //--- Initialize resize tooltip
      if (resize_hovered || resizing)                            //--- Check resize
      {
         ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get mode
         switch (active_mode)                                    //--- Switch mode
         {
            case BOTTOM: resize_tooltip = "Resize Bottom"; break; //--- Set bottom
            case RIGHT: resize_tooltip = "Resize Right"; break;  //--- Set right
            case BOTTOM_RIGHT: resize_tooltip = "Resize Bottom-Right"; break; //--- Set corner
            default: break;
         }
      }
      ObjectSetString(0, canvasGraphName, OBJPROP_TOOLTIP, resize_tooltip); //--- Set graph tooltip
      
      if (mouse_state == 1 && prev_mouse_state == 0)             //--- Check mouse down
      {
         if (header_hovered)                                     //--- Check header
         {
            panel_dragging = true;                               //--- Start drag
            panel_drag_x = mouse_x;                              //--- Set drag x
            panel_drag_y = mouse_y;                              //--- Set drag y
            panel_start_x = currentCanvasX;                      //--- Set start x
            panel_start_y = currentCanvasY;                      //--- Set start y
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);       //--- Disable scroll
            DrawHeaderOnCanvas();                                //--- Show drag color
            ChartRedraw();                                       //--- Redraw chart
         }
         else if (theme_hovered)                                 //--- Check theme
         {
            ToggleTheme();                                       //--- Toggle theme
         }
         else if (minimize_hovered)                              //--- Check minimize
         {
            ToggleMinimize();                                    //--- Toggle minimize
         }
         else if (close_hovered)                                 //--- Check close
         {
            CloseDashboard();                                    //--- Close dashboard
         }
         else                                                    //--- Handle resize
         {
            ENUM_RESIZE_MODE temp_mode = NONE;                   //--- Initialize temp
            if (!panel_dragging && !resizing && IsMouseOverResize(mouse_x, mouse_y, temp_mode)) //--- Check resize
            {
               resizing = true;                                  //--- Start resizing
               resize_mode = temp_mode;                          //--- Set mode
               resize_start_x = mouse_x;                         //--- Set start x
               resize_start_y = mouse_y;                         //--- Set start y
               start_width = currentWidth;                       //--- Set start width
               start_height = currentHeight;                     //--- Set start height
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);    //--- Disable scroll
               UpdateGraphOnCanvas();                            //--- Show icon
               ChartRedraw();                                    //--- Redraw chart
            }
         }
      }
      else if (panel_dragging && mouse_state == 1)               //--- Handle dragging
      {
         int dx = mouse_x - panel_drag_x;                        //--- Compute dx
         int dy = mouse_y - panel_drag_y;                        //--- Compute dy
         int new_x = panel_start_x + dx;                         //--- Compute new x
         int new_y = panel_start_y + dy;                         //--- Compute new y
         
         int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
         int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
         int full_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute full width
         int full_h = header_height + gap_y + (panels_minimized ? 0 : currentHeight); //--- Compute full height
         new_x = MathMax(0, MathMin(chart_w - full_w, new_x));   //--- Clamp x
         new_y = MathMax(0, MathMin(chart_h - full_h, new_y));   //--- Clamp y
         
         currentCanvasX = new_x;                                 //--- Update x
         currentCanvasY = new_y;                                 //--- Update y
         
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, new_x); //--- Update header x
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, new_y); //--- Update header y
         if (!panels_minimized)                                  //--- Check if shown
         {
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XDISTANCE, new_x); //--- Update graph x
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update graph y
            if (EnableStatsPanel)                                //--- Check stats
            {
               int statsX = new_x + currentWidth + PanelGap;     //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, statsX); //--- Update stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update stats y
            }
         }
         ChartRedraw();                                          //--- Redraw chart
      }
      else if (resizing && mouse_state == 1)                     //--- Handle resizing
      {
         int dx = mouse_x - resize_start_x;                      //--- Compute dx
         int dy = mouse_y - resize_start_y;                      //--- Compute dy
         int new_width = currentWidth;                           //--- Initialize new width
         int new_height = currentHeight;                         //--- Initialize new height
         if (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT) //--- Check right
         {
            new_width = MathMax(min_width, start_width + dx);    //--- Update width
         }
         if (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT) //--- Check bottom
         {
            new_height = MathMax(min_height, start_height + dy); //--- Update height
         }
         
         if (new_width != currentWidth || new_height != currentHeight) //--- Check change
         {
            int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
            int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
            int avail_w = chart_w - currentCanvasX;              //--- Compute available width
            int avail_h = chart_h - (currentCanvasY + header_height + gap_y); //--- Compute available height
            new_height = MathMin(new_height, avail_h);           //--- Clamp height
            if (EnableStatsPanel)                                //--- Check stats
            {
               double max_w_d = (avail_w - PanelGap) / 1.5;      //--- Compute max width
               int max_w = (int)MathFloor(max_w_d);              //--- Floor max
               new_width = MathMin(new_width, max_w);            //--- Clamp width
            }
            else                                                 //--- No stats
            {
               new_width = MathMin(new_width, avail_w);          //--- Clamp width
            }
            
            currentWidth = new_width;                            //--- Update width
            currentHeight = new_height;                          //--- Update height
            
            if (UseBackground && ArraySize(original_bg_pixels) > 0) //--- Check background
            {
               ArrayCopy(bg_pixels_graph, original_bg_pixels);   //--- Copy graph
               ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph
               if (EnableStatsPanel)                             //--- Check stats
               {
                  ArrayCopy(bg_pixels_stats, original_bg_pixels); //--- Copy stats
                  ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats
               }
            }
            
            canvasGraph.Resize(currentWidth, currentHeight);     //--- Resize graph
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XSIZE, currentWidth); //--- Update graph width
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YSIZE, currentHeight); //--- Update graph height
            
            if (EnableStatsPanel)                                //--- Check stats
            {
               int stats_width = currentWidth / 2;               //--- Compute stats width
               canvasStats.Resize(stats_width, currentHeight);   //--- Resize stats
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XSIZE, stats_width); //--- Update stats width
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YSIZE, currentHeight); //--- Update stats height
               int stats_x = currentCanvasX + currentWidth + PanelGap; //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, stats_x); //--- Update stats x
            }
            
            canvasHeader.Resize(currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0), header_height); //--- Resize header
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0)); //--- Update header width
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
            
            DrawHeaderOnCanvas();                                //--- Redraw header
            UpdateGraphOnCanvas();                               //--- Update graph
            if (EnableStatsPanel) UpdateStatsOnCanvas();         //--- Update stats
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      else if (mouse_state == 0 && prev_mouse_state == 1)        //--- Check mouse up
      {
         if (panel_dragging)                                     //--- Check dragging
         {
            panel_dragging = false;                              //--- Stop drag
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            DrawHeaderOnCanvas();                                //--- Reset color
            ChartRedraw();                                       //--- Redraw chart
         }
         if (resizing)                                           //--- Check resizing
         {
            resizing = false;                                    //--- Stop resize
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            UpdateGraphOnCanvas();                               //--- Remove icon
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      
      last_mouse_x = mouse_x;                                    //--- Update last x
      last_mouse_y = mouse_y;                                    //--- Update last y
      prev_mouse_state = mouse_state;                            //--- Update state
   }
}

In the OnChartEvent event handler, if id is CHARTEVENT_CHART_CHANGE, we call "DrawHeaderOnCanvas" to redraw the header, "UpdateGraphOnCanvas" for the graph, "UpdateStatsOnCanvas" if "EnableStatsPanel" is true, and ChartRedraw to refresh the display. For CHARTEVENT_MOUSE_MOVE, we cast lparam to mouse x, dparam to y, and sparam to state as integers. We store previous hover states in locals, then update "header_hovered" with "IsMouseOverHeader", "theme_hovered" with "IsMouseOverTheme", "minimize_hovered" with "IsMouseOverMinimize", "close_hovered" with "IsMouseOverClose", and "resize_hovered" with "IsMouseOverResize", passing a reference to "hover_mode". If resize hovered or "resizing" true, set "hover_mouse_local_x" and "hover_mouse_local_y" relative to graph position. We check if any hover state changed by comparing the previous to the current, and if so, redraw the header and graph, then redraw. Else if the resize state and the mouse position differ from "last_mouse_x"/"last_mouse_y", update the graph and redraw.

We initialize a header tooltip string, set to Toggle Theme (Dark/Light) if "theme_hovered", maximize/minimize message conditional on "panels_minimized" if "minimize_hovered", Close Dashboard if "close_hovered", and apply to "canvasHeaderName" with ObjectSetString using OBJPROP_TOOLTIP. Similarly, for the resize tooltip, determine active mode from "resize_mode" or "hover_mode", set the string based on bottom/right/bottom-right, and apply to "canvasGraphName".

If mouse state is one and "prev_mouse_state" zero for down press: if "header_hovered", set "panel_dragging" true, store drag/start coordinates, disable chart mouse scroll with ChartSetInteger "CHART_MOUSE_SCROLL" false, redraw header, redraw chart; else if "theme_hovered", call "ToggleTheme"; if "minimize_hovered", call "ToggleMinimize"; if "close_hovered", call "CloseDashboard"; else if not dragging/resizing and "IsMouseOverResize" true into temp mode, set "resizing" true, "resize_mode" to temp, store start coords/dims, disable scroll, update graph, redraw.

If "panel_dragging" true and state one for held, compute deltas, new x/y from start plus deltas, get chart width/height with ChartGetInteger  "CHART_WIDTH_IN_PIXELS"/CHART_HEIGHT_IN_PIXELS, full panel width/height including optional stats/gap/header if not minimized, clamp new x/y to zero up to chart minus full dims with "MathMax"/"MathMin", update "currentCanvasX"/"currentCanvasY", set object x/y distances for header, and if not minimized for graph and optional stats (computing stats x), then redraw.

If "resizing" is true and state one, compute deltas, initialize new width/height to current, add dx if right or corner mode clamped to "min_width", dy for bottom or corner to "min_height". If changed, get chart dims, available width/height from current position, clamp new height to available, for width if stats clamp to floored (available minus gap)/1.5 else to available, update currents. If background used, copy original pixels to graph/stats, scale with "ScaleImage" to new dims (stats half width). Resize "canvasGraph" with Resize and set "OBJPROP_XSIZE"/"YSIZE", similarly for stats if enabled, updating x position, and header to full width. Redraw header/graph/stats if enabled, redraw chart.

If state zero and "prev_mouse_state" one for up: if "panel_dragging", set false, enable scroll true, redraw header, redraw chart; if "resizing", set false, enable scroll, update graph, redraw chart. We update "last_mouse_x"/"last_mouse_y" to current, "prev_mouse_state" to state. We also need to update the dashboard per tick to reflect the new prices.

//+------------------------------------------------------------------+
//| Handle tick event                                                |
//+------------------------------------------------------------------+
void OnTick()
{
   static datetime lastBarTime = 0;                              //--- Initialize last time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);         //--- Get current time
   if (currentBarTime > lastBarTime)                             //--- Check new bar
   {
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
      lastBarTime = currentBarTime;                              //--- Update last time
   }
}

In the OnTick event handler, we use a static "lastBarTime" datetime initialized to zero to track the previous bar's open time, and get the current bar time with iTime for the symbol, period, and bar zero. If the current time is greater than "lastBarTime", indicating a new bar, we call "UpdateGraphOnCanvas" to refresh the price graph, "UpdateStatsOnCanvas" if "EnableStatsPanel" is true for stats, redraw the chart, and update "lastBarTime" to the current value. Finally, we need to delete rendered objects to avoid clutter when not needed.

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   canvasHeader.Destroy();                                       //--- Destroy header
   if (graphCreated) canvasGraph.Destroy();                      //--- Destroy graph if created
   if (statsCreated) canvasStats.Destroy();                      //--- Destroy stats if created
   ChartRedraw();                                                //--- Redraw chart
}

In the OnDeinit event handler, we destroy the header canvas with "canvasHeader.Destroy", then conditionally destroy the graph canvas if "graphCreated" is true using "canvasGraph.Destroy", and the stats canvas if "statsCreated" is true with "canvasStats.Destroy". Finally, we redraw the chart to ensure any remnants are cleared from the display. Upon compilation, we get the following outcome.

CANVAS DASHBOARD TEST

From the visualization, we can see that we have correctly set up the canvas dashboard with all the canvas components rendered correctly and interactable, hence achieving our objectives. What now remains is testing the workability of the system, and that is handled in the preceding section.


Backtesting

We did the testing, and below is the compiled visualization in a single Graphics Interchange Format (GIF) bitmap image format.

CANVAS DASHBOARD BACKTEST


Conclusion

In conclusion, we’ve developed a canvas-based price dashboard in MQL5 using the CCanvas class to create draggable and resizable panels for real-time price graphs with line plots, filled areas, and fog effects, alongside an optional stats panel for account metrics like balance/equity and current bar OHLC, supporting background images, gradients, theme toggling, and efficient event handling for interactivity. The system incorporates bicubic scaling for smooth resizing, alpha blending for overlays, and updates on new bars, providing a customizable tool for visual monitoring without native objects. In the next part, we will extend the dashboard by adding canvas based text screen for reading and scrolling that can integrate several text themes, with a dynamic scrollbar like the ones featured in the new upgraded MetaQuotes terminals. Keep tuned.

Attached files |
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Risk Management (Part 3): Building the Main Class for Risk Management Risk Management (Part 3): Building the Main Class for Risk Management
In this article, we will begin creating a core risk management class that will be key to controlling risks in the system. We will focus on building the foundations, defining the basic structures, variables and functions. In addition, we will implement the necessary methods for setting maximum profit and loss values, thereby laying the foundation for risk management.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Price Action Analysis Toolkit Development (Part 56): Reading Session Acceptance and Rejection with CPI Price Action Analysis Toolkit Development (Part 56): Reading Session Acceptance and Rejection with CPI
This article presents a session-based analytical framework that combines time-defined market sessions with the Candle Pressure Index (CPI) to classify acceptance and rejection behavior at session boundaries using closed-candle data and clearly defined rules.