﻿//+------------------------------------------------------------------+
//|    Canvas Graphing PART 3.2 - Statistical Distributions (3D).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>
#include <Canvas\Canvas3D.mqh>
#include <Canvas\DX\DXBox.mqh>
#include <Math\Stat\Binomial.mqh>
#include <Math\Stat\Math.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ResizeDirection
  {
   NO_RESIZE,           // No resize
   RESIZE_BOTTOM_EDGE,  // Resize bottom edge
   RESIZE_RIGHT_EDGE,   // Resize right edge
   RESIZE_CORNER        // Resize corner
  };

enum ViewModeType
  {
   VIEW_2D_MODE,  // 2D mode
   VIEW_3D_MODE   // 3D mode
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== VIEW MODE SETTINGS ==="
input ViewModeType     viewMode = VIEW_2D_MODE;              // View Mode (2D or 3D)

input group "=== DISTRIBUTION SETTINGS ==="
input int              numTrials = 40;                       // Number of Trials (n)
input double           successProbability = 0.75;            // Success Probability (p)
input int              sampleSize = 1000000;                 // Sample Size for Histogram
input int              histogramCells = 20;                  // Number of Histogram Cells
input int              histogramGapPixels = 2;               // Gap Between Histogram Bars (px)
input ENUM_TIMEFRAMES  chartTimeframe = PERIOD_CURRENT;      // Chart Timeframe

input group "=== CANVAS DISPLAY SETTINGS ==="
input int              initialCanvasX = 20;                  // Initial Canvas X Position
input int              initialCanvasY = 30;                  // Initial Canvas Y Position
input int              initialCanvasWidth = 600;             // Initial Canvas Width
input int              initialCanvasHeight = 400;            // Initial Canvas Height
input int              plotPadding = 10;                     // Plot Area Internal Padding (px)

input group "=== THEME COLOR (SINGLE CONTROL!) ==="
input color            themeColor = clrDodgerBlue;           // Master Theme Color
input bool             showBorderFrame = true;               // Show Border Frame

input group "=== HISTOGRAM AND CURVE SETTINGS ==="
input color            histogramColor = clrRed;              // Histogram Bar Color
input color            theoreticalCurveColor = clrBlue;      // Theoretical Curve Color
input int              curveLineWidth = 2;                   // Theoretical Curve Width

input group "=== BACKGROUND SETTINGS ==="
input bool             enableBackgroundFill = true;          // Enable Background Fill
input color            backgroundTopColor = clrWhite;        // Background Top Color
input double           backgroundOpacityLevel = 0.95;        // Background Opacity (0-1)

input group "=== TEXT AND LABELS ==="
input int              titleFontSize = 14;                   // Title Font Size
input color            titleTextColor = clrBlack;            // Title Text Color
input int              labelFontSize = 11;                   // Label Font Size
input color            labelTextColor = clrBlack;            // Label Text Color
input int              axisLabelFontSize = 12;               // Axis Labels Font Size
input bool             showStatistics = true;                // Show Statistics & Legend

input group "=== STATS & LEGEND PANEL SETTINGS ==="
input int              statsPanelX = 70;                     // Stats Panel X Position
input int              statsPanelY = 10;                     // Stats Panel Y Offset (from header)
input int              statsPanelWidth = 130;                // Stats Panel Width
input int              statsPanelHeight = 175;               // Stats Panel Height
input int              panelFontSize = 13;                   // Stats & Legend Font Size
input int              legendHeight = 35;                    // Legend Panel Height

input group "=== INTERACTION SETTINGS ==="
input bool             enableDragging = true;                // Enable Canvas Dragging
input bool             enableResizing = true;                // Enable Canvas Resizing
input int              resizeGripSize = 8;                   // Resize Grip Size (pixels)

input group "=== 3D VIEW SETTINGS ==="
input bool             autoFitCamera = true;                 // Auto-Fit Camera on Load
input double           initialCameraDistance = 60.0;         // Initial Camera Distance (3D)
input double           initialCameraAngleX = 0.6;            // Initial Camera Angle X (3D)
input double           initialCameraAngleY = 0.8;            // Initial Camera Angle Y (3D)

input group "=== 3D GROUND AND AXES SETTINGS ==="
input color            groundPlaneColor = clrLightGray;      // Ground Plane Color
input double           groundPlaneOpacity = 0.6;             // Ground Plane Opacity (0-1)
input float            groundPlaneWidth = 50.0;              // Ground Plane Width (X direction)
input float            groundPlaneDepth = 20.0;              // Ground Plane Depth (Z direction)
input float            groundPlaneThickness = 0.5;           // Ground Plane Thickness (Y direction)
input bool             show3DAxes = true;                    // Show 3D Axes
input color            axisXColor = clrRed;                  // X Axis Color
input color            axisYColor = clrGreen;                // Y Axis Color
input color            axisZColor = clrBlue;                 // Z Axis Color
input float            axisLength = 20.0;                    // Axis Length
input float            axisThickness = 0.1f;                 // Axis Thickness

input group "=== VIEW CUBE SETTINGS ==="
input bool             showViewCubeBackground = false;       // Show View Cube Background Panel

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
const int MIN_CANVAS_WIDTH  = 300;  // Minimum allowed canvas width
const int MIN_CANVAS_HEIGHT = 200;  // Minimum allowed canvas height
const int HEADER_BAR_HEIGHT = 35;   // Height of the header bar in pixels
const int SWITCH_ICON_SIZE  = 24;   // Size of the 2D/3D switch icon
const int SWITCH_ICON_MARGIN = 6;   // Margin around the switch icon
const int PAN_ICON_SIZE     = 24;   // Size of the pan mode toggle icon
const int PAN_ICON_MARGIN   = 6;    // Margin around the pan icon
const int VCUBE_SIZE        = 70;   // Pixel size of the view cube widget
const int VCUBE_MARGIN      = 6;    // Margin around the view cube widget

//+------------------------------------------------------------------+
//| Distribution visualization window class                          |
//+------------------------------------------------------------------+
class DistributionVisualizer
  {
protected:
   CCanvas3D         m_mainCanvas;            // Main 3D-capable canvas
   string            m_canvasObjectName;      // Chart object name for the canvas bitmap

   int               m_currentPositionX;      // Current X position of the canvas on chart
   int               m_currentPositionY;      // Current Y position of the canvas on chart
   int               m_currentWidth;          // Current canvas width in pixels
   int               m_currentHeight;         // Current canvas height in pixels

   bool              m_isDragging;            // True while user is dragging the canvas
   bool              m_isResizing;            // True while user is resizing the canvas
   int               m_dragStartX;            // Mouse X when drag began
   int               m_dragStartY;            // Mouse Y when drag began
   int               m_canvasStartX;          // Canvas X when drag began
   int               m_canvasStartY;          // Canvas Y when drag began
   int               m_resizeStartX;          // Mouse X when resize began
   int               m_resizeStartY;          // Mouse Y when resize began
   int               m_resizeInitialWidth;    // Canvas width at resize start
   int               m_resizeInitialHeight;   // Canvas height at resize start
   ResizeDirection   m_activeResizeMode;      // Currently active resize direction
   ResizeDirection   m_hoverResizeMode;       // Resize direction under cursor hover

   bool              m_isHoveringCanvas;      // True when mouse is over the canvas
   bool              m_isHoveringHeader;      // True when mouse is over the header bar
   bool              m_isHoveringResizeZone;  // True when mouse is in a resize grip zone
   bool              m_isHoveringSwitchIcon;  // True when mouse is over the mode switch icon
   bool              m_isHoveringPanIcon;     // True when mouse is over the pan mode icon
   bool              m_isHoveringViewCube;    // True when mouse is over the view cube widget
   int               m_lastMouseX;            // Last recorded mouse X coordinate
   int               m_lastMouseY;            // Last recorded mouse Y coordinate
   int               m_previousMouseButtonState; // Mouse button state on previous event

   ViewModeType      m_currentViewMode;       // Active view mode (2D or 3D)
   bool              m_are3DObjectsCreated;   // True once 3D scene objects have been built

   CDXBox            m_histogramBars[];       // Array of 3D boxes representing histogram bars
   CDXBox            m_groundPlane;           // 3D box used as the ground reference plane
   CDXBox            m_axisX;                 // 3D box representing the X axis
   CDXBox            m_axisY;                 // 3D box representing the Y axis
   CDXBox            m_axisZ;                 // 3D box representing the Z axis
   CDXBox            m_curveSegments[];       // Array of 3D boxes representing PMF curve tube segments
   int               m_curveSegmentCount;     // Number of active curve segment boxes

   double            m_cameraDistance;        // Distance from camera to scene target
   double            m_cameraAngleX;          // Camera elevation angle (radians)
   double            m_cameraAngleY;          // Camera azimuth angle (radians)
   int               m_mouse3DStartX;         // Mouse X when 3D orbit drag began
   int               m_mouse3DStartY;         // Mouse Y when 3D orbit drag began
   bool              m_isRotating3D;          // True while user is orbiting the 3D scene
   bool              m_panMode;               // True when pan mode is active (vs orbit mode)
   bool              m_isPanning;             // True while a pan drag is in progress
   int               m_panStartX;             // Mouse X when pan drag began
   int               m_panStartY;             // Mouse Y when pan drag began
   DXVector3         m_viewTarget;            // World-space point the camera looks at

   double            m_sampleData[];                  // Raw binomial sample values
   double            m_histogramIntervals[];           // Histogram bin centre positions
   double            m_histogramFrequencies[];         // Scaled frequency per histogram bin
   double            m_theoreticalXValues[];           // X values for the theoretical PMF curve
   double            m_theoreticalYValues[];           // Y values for the theoretical PMF curve
   double            m_minDataValue;                   // Minimum value in data range
   double            m_maxDataValue;                   // Maximum value in data range
   double            m_maxFrequency;                   // Peak raw frequency across all bins
   double            m_maxTheoreticalValue;            // Peak theoretical PMF value
   bool              m_isDataLoaded;                   // True once distribution data is ready

   double            m_sampleMean;                     // Computed sample mean
   double            m_sampleStandardDeviation;        // Computed sample standard deviation
   double            m_sampleSkewness;                 // Computed sample skewness
   double            m_sampleKurtosis;                 // Computed sample excess kurtosis
   double            m_percentile25;                   // 25th percentile (Q1)
   double            m_percentile50;                   // 50th percentile (median)
   double            m_percentile75;                   // 75th percentile (Q3)
   double            m_confidenceInterval95Lower;      // Lower bound of 95% confidence interval
   double            m_confidenceInterval95Upper;      // Upper bound of 95% confidence interval
   double            m_confidenceInterval99Lower;      // Lower bound of 99% confidence interval
   double            m_confidenceInterval99Upper;      // Upper bound of 99% confidence interval

   double            m_targetAngleX;          // Target camera elevation angle for animation
   double            m_targetAngleY;          // Target camera azimuth angle for animation
   bool              m_isAnimating;           // True while a camera snap animation is running
   int               m_animSteps;             // Total number of animation interpolation steps
   int               m_animStep;              // Current animation step index
   double            m_animStartAngleX;       // Camera elevation angle at animation start
   double            m_animStartAngleY;       // Camera azimuth angle at animation start
   string            m_vcubeHoverZone;        // Name of the view cube zone under the cursor
   int               m_vcubeCenterX;          // Screen X of the view cube widget centre
   int               m_vcubeCenterY;          // Screen Y of the view cube widget centre

public:
   //+------------------------------------------------------------------+
   //| Initialise all member variables to safe defaults                 |
   //+------------------------------------------------------------------+
   DistributionVisualizer(void)
     {
      //--- Set canvas object name
      m_canvasObjectName = "DistCanvas";
      //--- Set initial canvas X position from input
      m_currentPositionX = initialCanvasX;
      //--- Set initial canvas Y position from input
      m_currentPositionY = initialCanvasY;
      //--- Set initial canvas width from input
      m_currentWidth = initialCanvasWidth;
      //--- Set initial canvas height from input
      m_currentHeight = initialCanvasHeight;

      //--- Reset drag state
      m_isDragging = false;
      //--- Reset resize state
      m_isResizing = false;
      //--- Reset drag origin X
      m_dragStartX = 0;
      //--- Reset drag origin Y
      m_dragStartY = 0;
      //--- Reset canvas position snapshot X
      m_canvasStartX = 0;
      //--- Reset canvas position snapshot Y
      m_canvasStartY = 0;
      //--- Reset resize origin X
      m_resizeStartX = 0;
      //--- Reset resize origin Y
      m_resizeStartY = 0;
      //--- Reset width snapshot for resize
      m_resizeInitialWidth = 0;
      //--- Reset height snapshot for resize
      m_resizeInitialHeight = 0;
      //--- Reset active resize direction
      m_activeResizeMode = NO_RESIZE;
      //--- Reset hover resize direction
      m_hoverResizeMode = NO_RESIZE;

      //--- Reset canvas hover flag
      m_isHoveringCanvas = false;
      //--- Reset header hover flag
      m_isHoveringHeader = false;
      //--- Reset resize zone hover flag
      m_isHoveringResizeZone = false;
      //--- Reset switch icon hover flag
      m_isHoveringSwitchIcon = false;
      //--- Reset pan icon hover flag
      m_isHoveringPanIcon = false;
      //--- Reset view cube hover flag
      m_isHoveringViewCube = false;
      //--- Reset last mouse X
      m_lastMouseX = 0;
      //--- Reset last mouse Y
      m_lastMouseY = 0;
      //--- Reset previous mouse button state
      m_previousMouseButtonState = 0;

      //--- Set view mode from input
      m_currentViewMode = viewMode;
      //--- Mark 3D objects as not yet created
      m_are3DObjectsCreated = false;

      //--- Set camera distance from input
      m_cameraDistance = initialCameraDistance;
      //--- Set camera elevation angle from input
      m_cameraAngleX = initialCameraAngleX;
      //--- Set camera azimuth angle from input
      m_cameraAngleY = initialCameraAngleY;
      //--- Reset 3D orbit drag origin X
      m_mouse3DStartX = -1;
      //--- Reset 3D orbit drag origin Y
      m_mouse3DStartY = -1;
      //--- Mark 3D orbit as inactive
      m_isRotating3D = false;
      //--- Start in orbit mode (not pan)
      m_panMode = false;
      //--- Mark pan drag as inactive
      m_isPanning = false;
      //--- Reset pan drag origin X
      m_panStartX = 0;
      //--- Reset pan drag origin Y
      m_panStartY = 0;
      //--- Initialise view target at scene origin
      m_viewTarget = DXVector3(0.0f, 0.0f, 0.0f);

      //--- Reset data range minimum
      m_minDataValue = 0.0;
      //--- Reset data range maximum
      m_maxDataValue = 0.0;
      //--- Reset peak histogram frequency
      m_maxFrequency = 0.0;
      //--- Reset peak theoretical PMF value
      m_maxTheoreticalValue = 0.0;
      //--- Mark data as not yet loaded
      m_isDataLoaded = false;

      //--- Reset sample mean
      m_sampleMean = 0.0;
      //--- Reset standard deviation
      m_sampleStandardDeviation = 0.0;
      //--- Reset skewness
      m_sampleSkewness = 0.0;
      //--- Reset kurtosis
      m_sampleKurtosis = 0.0;
      //--- Reset 25th percentile
      m_percentile25 = 0.0;
      //--- Reset median
      m_percentile50 = 0.0;
      //--- Reset 75th percentile
      m_percentile75 = 0.0;
      //--- Reset 95% CI lower bound
      m_confidenceInterval95Lower = 0.0;
      //--- Reset 95% CI upper bound
      m_confidenceInterval95Upper = 0.0;
      //--- Reset 99% CI lower bound
      m_confidenceInterval99Lower = 0.0;
      //--- Reset 99% CI upper bound
      m_confidenceInterval99Upper = 0.0;

      //--- Reset animation target elevation
      m_targetAngleX = 0.0;
      //--- Reset animation target azimuth
      m_targetAngleY = 0.0;
      //--- Mark animation as inactive
      m_isAnimating = false;
      //--- Set default animation step count
      m_animSteps = 20;
      //--- Reset current animation step
      m_animStep = 0;
      //--- Reset animation start elevation
      m_animStartAngleX = 0.0;
      //--- Reset animation start azimuth
      m_animStartAngleY = 0.0;
      //--- Clear view cube hover zone
      m_vcubeHoverZone = "";
      //--- Reset view cube centre X
      m_vcubeCenterX = 0;
      //--- Reset view cube centre Y
      m_vcubeCenterY = 0;
      //--- Reset curve segment count
      m_curveSegmentCount = 0;
     }

   //+------------------------------------------------------------------+
   //| Release all 3D scene objects on destruction                      |
   //+------------------------------------------------------------------+
   ~DistributionVisualizer(void)
     {
      //--- Get total number of histogram bar boxes
      int count = ArraySize(m_histogramBars);
      //--- Release DirectX resources for every histogram bar
      for(int i = 0; i < count; i++)
         m_histogramBars[i].Shutdown();
      //--- Release DirectX resources for every curve tube segment
      for(int i = 0; i < m_curveSegmentCount; i++)
         m_curveSegments[i].Shutdown();
      //--- Release ground plane DirectX resources
      m_groundPlane.Shutdown();
      //--- Release X axis DirectX resources
      m_axisX.Shutdown();
      //--- Release Y axis DirectX resources
      m_axisY.Shutdown();
      //--- Release Z axis DirectX resources
      m_axisZ.Shutdown();
     }

   //+------------------------------------------------------------------+
   //| Create bitmap label, initialise 3D context and scene objects     |
   //+------------------------------------------------------------------+
   bool createCanvasAndObjects()
     {
      //--- Create the canvas bitmap label on the chart
      if(!m_mainCanvas.CreateBitmapLabel(m_canvasObjectName, 0, 0, m_currentWidth, m_currentHeight,
                                         COLOR_FORMAT_ARGB_NORMALIZE))
        {
         Print("ERROR: Failed to create canvas");
         return false;
        }
      //--- Position the canvas horizontally on the chart
      ObjectSetInteger(0, m_canvasObjectName, OBJPROP_XDISTANCE, m_currentPositionX);
      //--- Position the canvas vertically on the chart
      ObjectSetInteger(0, m_canvasObjectName, OBJPROP_YDISTANCE, m_currentPositionY);

      //--- Initialise the DirectX 3D rendering context
      if(!initialize3DContext())
        {
         Print("ERROR: Failed to initialize 3D context");
         return false;
        }
      //--- Build all 3D scene objects
      if(!create3DObjects())
        {
         Print("ERROR: Failed to create 3D objects");
         return false;
        }
      return true;
     }

   //+------------------------------------------------------------------+
   //| Set up projection, lighting and initial camera for 3D rendering  |
   //+------------------------------------------------------------------+
   bool initialize3DContext()
     {
      //--- Set perspective projection matrix with 30-degree FOV
      m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                       (float)m_currentWidth / (float)m_currentHeight,
                                       0.1f, 1000.0f);
      //--- Point the camera at the current view target
      m_mainCanvas.ViewTargetSet(m_viewTarget);
      //--- Define world up direction as positive Y
      m_mainCanvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f));
      //--- Set directional light colour to near-white
      m_mainCanvas.LightColorSet(DXColor(1.0f, 1.0f, 1.0f, 0.9f));
      //--- Set ambient light colour for soft fill
      m_mainCanvas.AmbientColorSet(DXColor(0.6f, 0.6f, 0.6f, 0.5f));

      //--- Auto-fit camera if enabled and data is already available
      if(autoFitCamera && m_isDataLoaded)
         autoFitCameraPosition();
      //--- Recompute and apply the camera transform
      updateCameraPosition();

      Print("SUCCESS: 3D context initialized");
      return true;
     }

   //+------------------------------------------------------------------+
   //| Build histogram bars, ground plane, axes and curve segments      |
   //+------------------------------------------------------------------+
   bool create3DObjects()
     {
      //--- Create all histogram bar boxes
      if(!create3DHistogramBars())
        {
         Print("ERROR: Failed to create 3D histogram bars");
         return false;
        }
      //--- Create the flat ground reference plane
      if(!createGroundPlane())
        {
         Print("ERROR: Failed to create ground plane");
         return false;
        }
      //--- Create coordinate axes only when enabled
      if(show3DAxes && !create3DAxes())
        {
         Print("ERROR: Failed to create 3D axes");
         return false;
        }
      //--- Attempt to create PMF curve tube segments (non-fatal if it fails)
      if(m_isDataLoaded && !create3DCurveSegments())
         Print("WARNING: Failed to create 3D curve segments");

      //--- Flag that all 3D objects are ready
      m_are3DObjectsCreated = true;
      Print("SUCCESS: 3D objects created");
      return true;
     }

   //+------------------------------------------------------------------+
   //| Prepare the scene for 3D rendering after a mode switch           |
   //+------------------------------------------------------------------+
   bool setup3DMode()
     {
      //--- If objects already exist, refresh camera and create any missing segments
      if(m_are3DObjectsCreated)
        {
         //--- Auto-fit camera to new data extents when enabled
         if(autoFitCamera && m_isDataLoaded)
            autoFitCameraPosition();
         //--- Apply updated camera transform
         updateCameraPosition();
         //--- Build curve segments if they were not created during init
         if(m_isDataLoaded && m_curveSegmentCount == 0)
            create3DCurveSegments();
         return true;
        }

      //--- Warn if this fallback path is reached unexpectedly
      Print("WARNING: 3D objects not created - this shouldn't happen!");
      //--- Attempt to build all objects as a fallback
      return create3DObjects();
     }

   //+------------------------------------------------------------------+
   //| Generate binomial sample, compute histogram and statistics       |
   //+------------------------------------------------------------------+
   bool loadDistributionData()
     {
      //--- Seed the random number generator with current tick count
      MathSrand(GetTickCount());
      //--- Allocate the sample data buffer
      ArrayResize(m_sampleData, sampleSize);
      //--- Fill the buffer with random binomial variates
      MathRandomBinomial(numTrials, successProbability, sampleSize, m_sampleData);

      //--- Compute the frequency histogram from the sample
      if(!computeHistogram(m_sampleData, m_histogramIntervals, m_histogramFrequencies,
                           m_maxDataValue, m_minDataValue, histogramCells))
        {
         Print("ERROR: Failed to calculate histogram");
         return false;
        }

      //--- Allocate arrays for the theoretical PMF curve
      ArrayResize(m_theoreticalXValues, numTrials + 1);
      ArrayResize(m_theoreticalYValues, numTrials + 1);
      //--- Fill X values as integer sequence 0 .. numTrials
      MathSequence(0, numTrials, 1, m_theoreticalXValues);
      //--- Compute binomial PMF for each X value
      MathProbabilityDensityBinomial(m_theoreticalXValues, numTrials, successProbability,
                                     false, m_theoreticalYValues);

      //--- Find the tallest histogram bin
      m_maxFrequency = m_histogramFrequencies[ArrayMaximum(m_histogramFrequencies)];
      //--- Find the peak theoretical PMF value
      m_maxTheoreticalValue = m_theoreticalYValues[ArrayMaximum(m_theoreticalYValues)];

      //--- Compute scaling factor to align histogram units with PMF units
      double scaleFactor = m_maxFrequency / m_maxTheoreticalValue;
      //--- Scale every bin frequency so it matches PMF units
      for(int i = 0; i < histogramCells; i++)
         m_histogramFrequencies[i] /= scaleFactor;

      //--- Compute all descriptive statistics
      computeAdvancedStatistics();
      //--- Mark data as successfully loaded
      m_isDataLoaded = true;

      //--- Update the 3D scene if it is already built
      if(m_currentViewMode == VIEW_3D_MODE && m_are3DObjectsCreated)
        {
         //--- Refit camera to new data extents
         if(autoFitCamera)
            autoFitCameraPosition();
         //--- Reposition every histogram bar in 3D space
         update3DHistogramBars();
         //--- Build curve segments on first load
         if(m_curveSegmentCount == 0)
            create3DCurveSegments();
         //--- Reposition every curve segment in 3D space
         update3DCurveSegments();
        }

      Print("SUCCESS: Loaded distribution data");
      return true;
     }

   //+------------------------------------------------------------------+
   //| Dispatch rendering to the active 2D or 3D pipeline              |
   //+------------------------------------------------------------------+
   void renderVisualization()
     {
      //--- Render using the 2D canvas pipeline
      if(m_currentViewMode == VIEW_2D_MODE)
         render2DVisualization();
      else
         //--- Render using the 3D DirectX pipeline
         render3DVisualization();
     }

   //+------------------------------------------------------------------+
   //| Advance the camera snap animation by one timer tick              |
   //+------------------------------------------------------------------+
   void tickAnimation()
     {
      //--- Abort if no animation is running
      if(!m_isAnimating) return;

      //--- Advance to the next animation step
      m_animStep++;
      //--- Compute normalised interpolation parameter [0, 1]
      double t = (double)m_animStep / (double)m_animSteps;
      //--- Apply smoothstep easing for a natural deceleration curve
      t = t * t * (3.0 - 2.0 * t);

      //--- Interpolate the elevation angle toward the target
      m_cameraAngleX = m_animStartAngleX + (m_targetAngleX - m_animStartAngleX) * t;
      //--- Interpolate the azimuth angle toward the target
      m_cameraAngleY = m_animStartAngleY + (m_targetAngleY - m_animStartAngleY) * t;

      //--- Snap to the exact target on the final step
      if(m_animStep >= m_animSteps)
        {
         m_cameraAngleX = m_targetAngleX;
         m_cameraAngleY = m_targetAngleY;
         m_isAnimating = false;
        }

      //--- Re-render and push the frame to the chart
      renderVisualization();
      ChartRedraw();
     }

   //+------------------------------------------------------------------+
   //| Process mouse move and button events for all interactions        |
   //+------------------------------------------------------------------+
   void handleMouseEvent(int mouseX, int mouseY, int mouseState)
     {
      //--- Snapshot previous hover states to detect changes requiring a redraw
      bool previousHoverState       = m_isHoveringCanvas;
      bool previousHeaderHoverState = m_isHoveringHeader;
      bool previousResizeHoverState = m_isHoveringResizeZone;
      bool previousSwitchHoverState = m_isHoveringSwitchIcon;
      bool previousPanHoverState    = m_isHoveringPanIcon;
      bool previousVCubeHoverState  = m_isHoveringViewCube;
      string previousVCubeZone      = m_vcubeHoverZone;

      //--- Update all hover flags for the current cursor position
      m_isHoveringCanvas = (mouseX >= m_currentPositionX &&
                            mouseX <= m_currentPositionX + m_currentWidth &&
                            mouseY >= m_currentPositionY &&
                            mouseY <= m_currentPositionY + m_currentHeight);
      m_isHoveringHeader      = isMouseOverHeaderBar(mouseX, mouseY);
      m_isHoveringSwitchIcon  = isMouseOverSwitchIcon(mouseX, mouseY);
      m_isHoveringPanIcon     = isMouseOverPanIcon(mouseX, mouseY);
      m_isHoveringResizeZone  = isMouseInResizeZone(mouseX, mouseY, m_hoverResizeMode);

      //--- Update view cube hover only in 3D mode
      if(m_currentViewMode == VIEW_3D_MODE)
         m_isHoveringViewCube = isMouseOverViewCube(mouseX, mouseY);
      else
         m_isHoveringViewCube = false;

      //--- Determine if any hover state change requires a redraw
      bool needRedraw = (previousHoverState       != m_isHoveringCanvas      ||
                         previousHeaderHoverState != m_isHoveringHeader      ||
                         previousResizeHoverState != m_isHoveringResizeZone  ||
                         previousSwitchHoverState != m_isHoveringSwitchIcon  ||
                         previousPanHoverState    != m_isHoveringPanIcon     ||
                         previousVCubeHoverState  != m_isHoveringViewCube    ||
                         previousVCubeZone        != m_vcubeHoverZone);

      //--- Handle 3D orbit and pan drags when over the canvas body (not header or cube)
      if(m_currentViewMode == VIEW_3D_MODE && m_isHoveringCanvas &&
         !m_isHoveringHeader && !m_isHoveringViewCube)
        {
         //--- Orbit mode: rotate the camera around the target
         if(!m_panMode)
           {
            //--- Begin orbit on fresh left-button press
            if(mouseState == 1 && m_previousMouseButtonState == 0)
              {
               m_isRotating3D  = true;
               m_mouse3DStartX = mouseX;
               m_mouse3DStartY = mouseY;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
              }
            //--- Continue orbit while button is held
            else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isRotating3D)
              {
               //--- Update azimuth proportional to horizontal mouse delta
               m_cameraAngleY += (mouseX - m_mouse3DStartX) / 300.0;
               //--- Update elevation proportional to vertical mouse delta
               m_cameraAngleX += (mouseY - m_mouse3DStartY) / 300.0;
               //--- Clamp elevation to avoid gimbal lock at poles
               if(m_cameraAngleX < -DX_PI * 0.499) m_cameraAngleX = -DX_PI * 0.499;
               if(m_cameraAngleX >  DX_PI * 0.499) m_cameraAngleX =  DX_PI * 0.499;
               //--- Update anchor for next delta computation
               m_mouse3DStartX = mouseX;
               m_mouse3DStartY = mouseY;
               //--- Cancel any running snap animation
               m_isAnimating = false;
               needRedraw = true;
              }
            //--- End orbit on button release
            else if(mouseState == 0 && m_previousMouseButtonState == 1)
              {
               m_isRotating3D = false;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
              }
           }
         else
           {
            //--- Pan mode: translate the camera target in the view plane
            if(mouseState == 1 && m_previousMouseButtonState == 0)
              {
               //--- Begin pan drag
               m_isPanning = true;
               m_panStartX = mouseX;
               m_panStartY = mouseY;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
              }
            else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isPanning)
              {
               //--- Compute mouse delta since last event
               int deltaX = mouseX - m_panStartX;
               int deltaY = mouseY - m_panStartY;

               //--- Reconstruct the camera position vector in world space
               DXVector4 camera(0.0f, 0.0f, (float)-m_cameraDistance, 1.0f);
               DXMatrix rotX;
               DXMatrixRotationX(rotX, (float)m_cameraAngleX);
               DXVec4Transform(camera, camera, rotX);
               DXMatrix rotY;
               DXMatrixRotationY(rotY, (float)m_cameraAngleY);
               DXVec4Transform(camera, camera, rotY);
               DXVector3 cameraPos(camera.x, camera.y, camera.z);

               //--- Compute and normalise the forward direction toward the target
               DXVector3 forward;
               DXVec3Subtract(forward, m_viewTarget, cameraPos);
               float len = DXVec3Length(forward);
               if(len > 0.0f) DXVec3Scale(forward, forward, 1.0f / len);

               //--- Compute the camera right vector via cross product
               DXVector3 worldUp(0.0f, 1.0f, 0.0f);
               DXVector3 right;
               DXVec3Cross(right, worldUp, forward);
               len = DXVec3Length(right);
               if(len > 0.0f) DXVec3Scale(right, right, 1.0f / len);

               //--- Compute the camera up vector orthogonal to forward and right
               DXVector3 camUp;
               DXVec3Cross(camUp, forward, right);
               len = DXVec3Length(camUp);
               if(len > 0.0f) DXVec3Scale(camUp, camUp, 1.0f / len);

               //--- Scale the pan movement proportional to camera distance
               float panFactor = (float)m_cameraDistance / 500.0f;
               DXVector3 moveRight;
               DXVec3Scale(moveRight, right,  (float)(-deltaX) * panFactor);
               DXVector3 moveUp;
               DXVec3Scale(moveUp,   camUp,   (float)( deltaY) * panFactor);

               //--- Translate the view target by the computed pan offset
               DXVector3 temp;
               DXVec3Add(temp,        m_viewTarget, moveRight);
               DXVec3Add(m_viewTarget, temp,        moveUp);

               //--- Update pan anchor for next delta computation
               m_panStartX = mouseX;
               m_panStartY = mouseY;
               needRedraw = true;
              }
            //--- End pan drag on button release
            else if(mouseState == 0 && m_previousMouseButtonState == 1)
              {
               m_isPanning = false;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
              }
           }
        }

      //--- Handle button-press interactions for UI elements
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         //--- Handle view cube face/edge/corner click in 3D mode
         if(m_isHoveringViewCube && m_currentViewMode == VIEW_3D_MODE && m_vcubeHoverZone != "")
           {
            handleViewCubeClick(mouseX, mouseY);
            needRedraw = true;
            m_previousMouseButtonState = mouseState;
            return;
           }
         //--- Toggle pan mode when the pan icon is clicked
         else if(m_isHoveringPanIcon && m_currentViewMode == VIEW_3D_MODE)
           {
            m_panMode = !m_panMode;
            needRedraw = true;
            m_previousMouseButtonState = mouseState;
            return;
           }
         //--- Switch the 2D/3D view mode when the switch icon is clicked
         else if(m_isHoveringSwitchIcon)
           {
            switchViewMode();
            m_previousMouseButtonState = mouseState;
            return;
           }
         //--- Begin canvas drag when clicking the header (not a resize zone)
         else if(enableDragging && m_isHoveringHeader && !m_isHoveringResizeZone)
           {
            m_isDragging   = true;
            m_dragStartX   = mouseX;
            m_dragStartY   = mouseY;
            m_canvasStartX = m_currentPositionX;
            m_canvasStartY = m_currentPositionY;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            needRedraw = true;
           }
         //--- Begin canvas resize when clicking a resize grip zone
         else if(m_isHoveringResizeZone)
           {
            m_isResizing          = true;
            m_activeResizeMode    = m_hoverResizeMode;
            m_resizeStartX        = mouseX;
            m_resizeStartY        = mouseY;
            m_resizeInitialWidth  = m_currentWidth;
            m_resizeInitialHeight = m_currentHeight;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            needRedraw = true;
           }
        }
      //--- Continue drag or resize while button stays pressed
      else if(mouseState == 1 && m_previousMouseButtonState == 1)
        {
         if(m_isDragging)
            handleCanvasDrag(mouseX, mouseY);
         else if(m_isResizing)
            handleCanvasResize(mouseX, mouseY);
        }
      //--- End drag or resize on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         if(m_isDragging || m_isResizing)
           {
            m_isDragging       = false;
            m_isResizing       = false;
            m_activeResizeMode = NO_RESIZE;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            needRedraw = true;
           }
        }

      //--- Redraw the visualization if any state changed
      if(needRedraw)
        {
         renderVisualization();
         ChartRedraw();
        }

      //--- Record current mouse position for next event
      m_lastMouseX = mouseX;
      m_lastMouseY = mouseY;
      //--- Record current button state for next event
      m_previousMouseButtonState = mouseState;
     }

   //+------------------------------------------------------------------+
   //| Handle mouse wheel zoom for the 3D scene                        |
   //+------------------------------------------------------------------+
   void handleMouseWheel(int mouseX, int mouseY, double delta)
     {
      //--- Determine if the wheel event occurred over the 3D canvas body
      bool isOverCanvas = (mouseX >= m_currentPositionX &&
                           mouseX <= m_currentPositionX + m_currentWidth &&
                           mouseY >= m_currentPositionY + HEADER_BAR_HEIGHT &&
                           mouseY <= m_currentPositionY + m_currentHeight);

      //--- Apply zoom only in 3D mode when the cursor is over the canvas
      if(m_currentViewMode == VIEW_3D_MODE && isOverCanvas)
        {
         //--- Suppress chart scroll so the wheel is captured by the visualizer
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         //--- Adjust camera distance proportional to the wheel delta
         m_cameraDistance *= 1.0 - delta * 0.001;
         //--- Clamp to prevent clipping through the scene
         if(m_cameraDistance <  20.0) m_cameraDistance =  20.0;
         if(m_cameraDistance > 200.0) m_cameraDistance = 200.0;
         //--- Re-render with updated camera distance
         renderVisualization();
         ChartRedraw();
        }
      else
        {
         //--- Restore chart scroll when wheel is outside the canvas
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }

   //+------------------------------------------------------------------+
   //| Return true when the cursor is over the mode switch icon         |
   //+------------------------------------------------------------------+
   bool isMouseOverSwitchIcon(int mouseX, int mouseY)
     {
      //--- Compute the icon's left edge from the canvas right margin
      int iconX = m_currentPositionX + m_currentWidth - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
      //--- Vertically centre the icon within the header bar
      int iconY = m_currentPositionY + (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;
      //--- Return true if the cursor falls within the icon bounding box
      return (mouseX >= iconX && mouseX <= iconX + SWITCH_ICON_SIZE &&
              mouseY >= iconY && mouseY <= iconY + SWITCH_ICON_SIZE);
     }

   //+------------------------------------------------------------------+
   //| Return true when the cursor is over the pan mode toggle icon     |
   //+------------------------------------------------------------------+
   bool isMouseOverPanIcon(int mouseX, int mouseY)
     {
      //--- Pan icon is only visible in 3D mode
      if(m_currentViewMode != VIEW_3D_MODE) return false;
      //--- Align pan icon with the right edge below the header
      int iconX = m_currentPositionX + m_currentWidth - PAN_ICON_SIZE - PAN_ICON_MARGIN;
      int iconY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
      //--- Return true if the cursor falls within the icon bounding box
      return (mouseX >= iconX && mouseX <= iconX + PAN_ICON_SIZE &&
              mouseY >= iconY && mouseY <= iconY + PAN_ICON_SIZE);
     }

   //+------------------------------------------------------------------+
   //| Return true when the cursor is over the view cube widget         |
   //+------------------------------------------------------------------+
   bool isMouseOverViewCube(int mouseX, int mouseY)
     {
      //--- View cube is only visible in 3D mode
      if(m_currentViewMode != VIEW_3D_MODE) return false;
      //--- Compute the view cube bounding area below the pan icon
      int cubeAreaX = m_currentPositionX + m_currentWidth - VCUBE_SIZE - VCUBE_MARGIN;
      int cubeAreaY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
      int cubeAreaW = VCUBE_SIZE;
      int cubeAreaH = VCUBE_SIZE;

      //--- Check if the cursor is within the bounding box
      if(mouseX >= cubeAreaX && mouseX <= cubeAreaX + cubeAreaW &&
         mouseY >= cubeAreaY && mouseY <= cubeAreaY + cubeAreaH)
        {
         //--- Detect the specific cube face, edge or corner under the cursor
         detectViewCubeZone(mouseX - m_currentPositionX, mouseY - m_currentPositionY);
         return (m_vcubeHoverZone != "");
        }
      //--- Cursor is outside the view cube; clear hover zone
      m_vcubeHoverZone = "";
      return false;
     }

   //+------------------------------------------------------------------+
   //| Toggle between 2D and 3D view modes                             |
   //+------------------------------------------------------------------+
   void switchViewMode()
     {
      //--- Switch from 2D to 3D
      if(m_currentViewMode == VIEW_2D_MODE)
        {
         m_currentViewMode = VIEW_3D_MODE;
         Print("Switched to 3D mode");
         //--- Set up the 3D scene; revert to 2D on failure
         if(!setup3DMode())
           {
            Print("ERROR: Failed to setup 3D mode, reverting to 2D");
            m_currentViewMode = VIEW_2D_MODE;
           }
         else
           {
            //--- Auto-fit the camera to the scene on mode entry
            if(autoFitCamera)
               autoFitCameraPosition();
           }
        }
      else
        {
         //--- Switch from 3D back to 2D and reset pan mode
         m_currentViewMode = VIEW_2D_MODE;
         m_panMode = false;
         Print("Switched to 2D mode");
        }
      //--- Render the scene in the new mode immediately
      renderVisualization();
      ChartRedraw();
     }

   //+------------------------------------------------------------------+
   //| Deactivate pan mode and trigger a redraw                        |
   //+------------------------------------------------------------------+
   void exitPanMode()
     {
      //--- Only act if pan mode is currently active
      if(m_panMode)
        {
         m_panMode = false;
         renderVisualization();
         ChartRedraw();
        }
     }

   //+------------------------------------------------------------------+
   //| Allocate and configure one DXBox per histogram bin              |
   //+------------------------------------------------------------------+
   bool create3DHistogramBars()
     {
      //--- Allocate bar array to match the required bin count
      ArrayResize(m_histogramBars, histogramCells);
      //--- Decompose histogram colour into RGB byte components
      uchar r = (uchar)((histogramColor)       & 0xFF);
      uchar g = (uchar)((histogramColor >> 8)  & 0xFF);
      uchar b = (uchar)((histogramColor >> 16) & 0xFF);

      //--- Create and configure each bar box
      for(int i = 0; i < histogramCells; i++)
        {
         //--- Create unit box; transform applied later in update step
         if(!m_histogramBars[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                       DXVector3(-0.5f, 0.0f, -0.5f),
                                       DXVector3( 0.5f, 1.0f,  0.5f)))
           {
            Print("ERROR: Failed to create 3D box for bar ", i);
            return false;
           }
         //--- Set the bar's base diffuse colour from the input colour
         m_histogramBars[i].DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f));
         //--- Add a subtle specular highlight for depth perception
         m_histogramBars[i].SpecularColorSet(DXColor(0.2f, 0.2f, 0.2f, 0.3f));
         //--- Set specular shininess exponent
         m_histogramBars[i].SpecularPowerSet(32.0f);
         //--- Disable self-emission (bars lit by scene lights only)
         m_histogramBars[i].EmissionColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
         //--- Register the bar with the 3D scene
         m_mainCanvas.ObjectAdd(GetPointer(m_histogramBars[i]));
        }
      return true;
     }

   //+------------------------------------------------------------------+
   //| Compute an optimal camera distance and angles for the scene      |
   //+------------------------------------------------------------------+
   void autoFitCameraPosition()
     {
      //--- Abort if not in 3D mode or data has not been loaded
      if(m_currentViewMode != VIEW_3D_MODE || !m_isDataLoaded) return;

      //--- Fixed scene width used for distance estimation
      float totalWidth   = 30.0f;
      //--- Track the tallest bar in the scene
      float maxBarHeight = 0.0f;
      //--- Use the peak PMF value as the Y scale reference
      double rangeY = m_maxTheoreticalValue;
      if(rangeY == 0) rangeY = 1;

      //--- Find the maximum normalised bar height across all bins
      for(int i = 0; i < histogramCells; i++)
        {
         float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
         float barHeight = normalizedHeight * 15.0f;
         if(barHeight > maxBarHeight) maxBarHeight = barHeight;
        }

      //--- Determine bounding extents for the entire scene
      float sceneWidth  = totalWidth;
      float sceneHeight = MathMax(maxBarHeight, 15.0f);
      float sceneDepth  = 10.0f;

      //--- Compute the scene bounding diagonal for FOV-based fitting
      float diagonal = MathSqrt(sceneWidth  * sceneWidth  +
                                sceneHeight * sceneHeight +
                                sceneDepth  * sceneDepth);
      //--- Match the projection FOV used in initialize3DContext
      float fov = (float)(DX_PI / 6.0);
      //--- Derive camera distance so the scene fills the view with a 1.5x margin
      m_cameraDistance = (diagonal / 2.0f) / MathTan(fov / 2.0f) * 1.5;

      //--- Set comfortable default elevation and azimuth angles
      m_cameraAngleX = 0.5;
      m_cameraAngleY = 0.7;

      //--- Enforce minimum distance to avoid near-plane clipping
      if(m_cameraDistance <  35.0) m_cameraDistance =  35.0;
      //--- Enforce maximum distance to keep bars visible
      if(m_cameraDistance > 100.0) m_cameraDistance = 100.0;

      Print("Auto-fit camera: Distance = ", m_cameraDistance,
            ", AngleX = ", m_cameraAngleX, ", AngleY = ", m_cameraAngleY);
     }

   //+------------------------------------------------------------------+
   //| Create the flat ground reference plane in 3D space              |
   //+------------------------------------------------------------------+
   bool createGroundPlane()
     {
      //--- Build a thin box spanning the configured width and depth
      if(!m_groundPlane.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                               DXVector3(-groundPlaneWidth / 2.0f, -groundPlaneThickness, -groundPlaneDepth / 2.0f),
                               DXVector3( groundPlaneWidth / 2.0f,  0.0f,                  groundPlaneDepth / 2.0f)))
        {
         Print("ERROR: Failed to create ground plane");
         return false;
        }
      //--- Decompose ground colour into RGB byte components (BGR layout)
      uchar r = (uchar)((groundPlaneColor >> 16) & 0xFF);
      uchar g = (uchar)((groundPlaneColor >> 8)  & 0xFF);
      uchar b = (uchar)( groundPlaneColor         & 0xFF);
      //--- Apply the configured ground colour and opacity
      m_groundPlane.DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f,
                                            (float)groundPlaneOpacity));
      //--- Remove specular highlight for a flat matte appearance
      m_groundPlane.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
      //--- Register the ground plane with the 3D scene
      m_mainCanvas.ObjectAdd(GetPointer(m_groundPlane));
      return true;
     }

   //+------------------------------------------------------------------+
   //| Create colour-coded X, Y, and Z coordinate axis boxes           |
   //+------------------------------------------------------------------+
   bool create3DAxes()
     {
      //--- Create X axis as a thin horizontal box along positive X
      if(!m_axisX.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                         DXVector3(0.0f, 0.0f, 0.0f),
                         DXVector3(axisLength, axisThickness, axisThickness)))
        {
         Print("ERROR: Failed to create X axis");
         return false;
        }
      //--- Extract X axis colour components
      uchar rx = (uchar)((axisXColor >> 16) & 0xFF);
      uchar gx = (uchar)((axisXColor >> 8)  & 0xFF);
      uchar bx = (uchar)( axisXColor         & 0xFF);
      //--- Apply X axis diffuse colour
      m_axisX.DiffuseColorSet(DXColor(rx / 255.0f, gx / 255.0f, bx / 255.0f, 1.0f));
      m_axisX.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
      m_mainCanvas.ObjectAdd(GetPointer(m_axisX));

      //--- Create Y axis as a thin vertical box along positive Y
      if(!m_axisY.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                         DXVector3(0.0f, 0.0f, 0.0f),
                         DXVector3(axisThickness, axisLength, axisThickness)))
        {
         Print("ERROR: Failed to create Y axis");
         return false;
        }
      //--- Extract Y axis colour components
      uchar ry = (uchar)((axisYColor >> 16) & 0xFF);
      uchar gy = (uchar)((axisYColor >> 8)  & 0xFF);
      uchar by = (uchar)( axisYColor         & 0xFF);
      //--- Apply Y axis diffuse colour
      m_axisY.DiffuseColorSet(DXColor(ry / 255.0f, gy / 255.0f, by / 255.0f, 1.0f));
      m_axisY.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
      m_mainCanvas.ObjectAdd(GetPointer(m_axisY));

      //--- Create Z axis as a thin depth box along positive Z
      if(!m_axisZ.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                         DXVector3(0.0f, 0.0f, 0.0f),
                         DXVector3(axisThickness, axisThickness, axisLength)))
        {
         Print("ERROR: Failed to create Z axis");
         return false;
        }
      //--- Extract Z axis colour components
      uchar rz = (uchar)((axisZColor >> 16) & 0xFF);
      uchar gz = (uchar)((axisZColor >> 8)  & 0xFF);
      uchar bz = (uchar)( axisZColor         & 0xFF);
      //--- Apply Z axis diffuse colour
      m_axisZ.DiffuseColorSet(DXColor(rz / 255.0f, gz / 255.0f, bz / 255.0f, 1.0f));
      m_axisZ.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
      m_mainCanvas.ObjectAdd(GetPointer(m_axisZ));
      return true;
     }

   //+------------------------------------------------------------------+
   //| Allocate one DXBox tube segment per PMF curve interval          |
   //+------------------------------------------------------------------+
   bool create3DCurveSegments()
     {
      //--- Get the total number of PMF sample points
      int numPoints = ArraySize(m_theoreticalXValues);
      Print("DEBUG: create3DCurveSegments called, numPoints=", numPoints);
      //--- Need at least two points to form a segment
      if(numPoints < 2) return false;

      //--- Shutdown any previously created segments before rebuilding
      for(int i = 0; i < m_curveSegmentCount; i++)
         m_curveSegments[i].Shutdown();

      //--- One segment per adjacent pair of PMF points
      m_curveSegmentCount = numPoints - 1;
      ArrayResize(m_curveSegments, m_curveSegmentCount);

      //--- Decompose PMF curve colour into RGB byte components
      uchar cr = (uchar)((theoreticalCurveColor)       & 0xFF);
      uchar cg = (uchar)((theoreticalCurveColor >> 8)  & 0xFF);
      uchar cb = (uchar)((theoreticalCurveColor >> 16) & 0xFF);

      //--- Create and configure each tube segment box
      for(int i = 0; i < m_curveSegmentCount; i++)
        {
         //--- Create unit box; actual transform applied in update step
         if(!m_curveSegments[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                       DXVector3(0.0f, -0.5f, -0.5f),
                                       DXVector3(1.0f,  0.5f,  0.5f)))
           {
            Print("ERROR: Failed to create curve segment ", i);
            return false;
           }
         //--- Set the segment diffuse colour from the curve colour input
         m_curveSegments[i].DiffuseColorSet(DXColor(cr / 255.0f, cg / 255.0f, cb / 255.0f, 1.0f));
         //--- Add a specular highlight to the tube surface
         m_curveSegments[i].SpecularColorSet(DXColor(0.3f, 0.3f, 0.3f, 0.5f));
         //--- Set specular shininess exponent
         m_curveSegments[i].SpecularPowerSet(32.0f);
         //--- Add a faint self-emission for visibility in shadowed areas
         m_curveSegments[i].EmissionColorSet(DXColor(cr / 255.0f * 0.25f,
                                                      cg / 255.0f * 0.25f,
                                                      cb / 255.0f * 0.25f, 1.0f));
         //--- Register the segment with the 3D scene
         m_mainCanvas.ObjectAdd(GetPointer(m_curveSegments[i]));
        }

      Print("DEBUG: Created ", m_curveSegmentCount, " curve segments successfully");
      return true;
     }

   //+------------------------------------------------------------------+
   //| Reposition and orient every PMF curve tube segment to match data |
   //+------------------------------------------------------------------+
   void update3DCurveSegments()
     {
      //--- Abort if data or segments are not ready
      if(!m_isDataLoaded || m_curveSegmentCount == 0) return;

      //--- Compute data ranges for world-space mapping
      double rangeX = m_maxDataValue - m_minDataValue;
      double rangeY = m_maxTheoreticalValue;
      if(rangeX == 0) rangeX = 1;
      if(rangeY == 0) rangeY = 1;

      //--- Match the total scene width used for the histogram bars
      float totalWidth  = 30.0f;
      float offsetX     = -totalWidth / 2.0f;
      float barSpacing  = totalWidth / (float)histogramCells;
      float bw          = barSpacing * 0.8f;
      //--- Tube radius controls the 3D thickness of each curve segment
      float tubeRadius  = 0.2f;
      //--- Place the curve in front of the histogram bars along Z
      float zPos        = bw / 2.0f + 0.5f;

      //--- Limit debug logging to the first call only
      static bool firstDebug = true;

      //--- Build and apply a custom transform matrix for each segment
      for(int i = 0; i < m_curveSegmentCount; i++)
        {
         //--- Map PMF X and Y values to 3D world space coordinates
         float x1 = offsetX + (float)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * totalWidth);
         float y1 = (float)(m_theoreticalYValues[i]     / rangeY * 15.0);
         float x2 = offsetX + (float)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * totalWidth);
         float y2 = (float)(m_theoreticalYValues[i + 1] / rangeY * 15.0);

         //--- Compute the segment direction vector components
         float dx = x2 - x1;
         float dy = y2 - y1;
         //--- Compute segment length for normalisation
         float segLen = (float)MathSqrt(dx * dx + dy * dy);
         if(segLen < 0.0001f) segLen = 0.0001f;
         //--- Normalise the direction vector
         float dirX = dx / segLen;
         float dirY = dy / segLen;

         //--- Log the first segment only for diagnostics
         if(firstDebug && i == 0)
            Print("DEBUG curve seg0: x1=", x1, " y1=", y1, " x2=", x2, " y2=", y2,
                  " len=", segLen, " z=", zPos);

         //--- Build a custom transform that scales, rotates and translates the unit box
         //--- The transform rows encode: [right, up, depth, translation]
         DXMatrix transform;
         transform.m[0][0] = dirX * segLen;      transform.m[0][1] = dirY * segLen;      transform.m[0][2] = 0.0f;         transform.m[0][3] = 0.0f;
         transform.m[1][0] = -dirY * tubeRadius; transform.m[1][1] = dirX * tubeRadius;  transform.m[1][2] = 0.0f;         transform.m[1][3] = 0.0f;
         transform.m[2][0] = 0.0f;               transform.m[2][1] = 0.0f;               transform.m[2][2] = tubeRadius;   transform.m[2][3] = 0.0f;
         transform.m[3][0] = x1;                 transform.m[3][1] = y1;                 transform.m[3][2] = zPos;         transform.m[3][3] = 1.0f;

         //--- Apply the combined transform to this curve segment
         m_curveSegments[i].TransformMatrixSet(transform);
        }
      //--- Suppress first-call debug logging after the first pass
      firstDebug = false;
     }

   //+------------------------------------------------------------------+
   //| Recompute and apply the view and light matrices from angles      |
   //+------------------------------------------------------------------+
   void updateCameraPosition()
     {
      //--- Only apply in 3D mode
      if(m_currentViewMode != VIEW_3D_MODE) return;

      //--- Start with a camera positioned along the negative Z axis
      DXVector4 camera = DXVector4(0.0f, 0.0f, (float)(-m_cameraDistance), 1.0f);

      //--- Rotate camera around the X axis by the elevation angle
      DXMatrix rotationX;
      DXMatrixRotationX(rotationX, (float)m_cameraAngleX);
      DXVec4Transform(camera, camera, rotationX);

      //--- Rotate the result around the Y axis by the azimuth angle
      DXMatrix rotationY;
      DXMatrixRotationY(rotationY, (float)m_cameraAngleY);
      DXVec4Transform(camera, camera, rotationY);

      //--- Offset the camera position by the current view target (for pan support)
      DXVector3 cameraPos(camera.x, camera.y, camera.z);
      DXVector3 adjustedPos;
      DXVec3Add(adjustedPos, cameraPos, m_viewTarget);

      //--- Apply the final camera world position and target
      m_mainCanvas.ViewPositionSet(adjustedPos);
      m_mainCanvas.ViewTargetSet(m_viewTarget);

      //--- Place the key light slightly above the camera for natural shading
      DXVector3 lightPos = DXVector3(adjustedPos.x, adjustedPos.y + 10.0f, adjustedPos.z);
      DXVector3 target   = m_viewTarget;

      //--- Compute and normalise the light direction vector
      DXVector3 lightDir;
      DXVec3Subtract(lightDir, target, lightPos);
      float length = DXVec3Length(lightDir);
      if(length > 0.0f)
         DXVec3Scale(lightDir, lightDir, 1.0f / length);

      //--- Apply the normalised light direction to the scene
      m_mainCanvas.LightDirectionSet(lightDir);
     }

   //+------------------------------------------------------------------+
   //| Reposition and scale every 3D histogram bar to match data        |
   //+------------------------------------------------------------------+
   void update3DHistogramBars()
     {
      //--- Abort if data has not been loaded
      if(!m_isDataLoaded) return;

      //--- Compute the X data range for spatial mapping
      double rangeX = m_maxDataValue - m_minDataValue;
      //--- Use the peak PMF value as the height scale reference
      double rangeY = m_maxTheoreticalValue;
      if(rangeX == 0) rangeX = 1;
      if(rangeY == 0) rangeY = 1;

      //--- Total scene width used to space the bars evenly
      float totalWidth = 30.0f;
      float barSpacing = totalWidth / (float)histogramCells;
      //--- Make each bar 80% of its slot to leave a small gap
      float barWidth   = barSpacing * 0.8f;
      //--- Shift origin so bars are centred on the scene
      float offsetX    = -totalWidth / 2.0f;

      //--- Update scale and position of every bar
      for(int i = 0; i < histogramCells; i++)
        {
         //--- Normalise this bin's frequency against the peak PMF
         float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
         //--- Map to scene height units (max 15 units tall)
         float barHeight = normalizedHeight * 15.0f;
         //--- Enforce a minimum visible height
         if(barHeight < 0.5f) barHeight = 0.5f;
         //--- Compute the bar's X centre in scene space
         float xPos = offsetX + (float)i * barSpacing + barWidth / 2.0f;

         //--- Build scale, translation and combined transform matrices
         DXMatrix scale, translation, transform;
         DXMatrixScaling(scale, barWidth, barHeight, barWidth);
         DXMatrixTranslation(translation, xPos, 0.0f, 0.0f);
         DXMatrixMultiply(transform, scale, translation);
         //--- Apply the combined world transform to this bar
         m_histogramBars[i].TransformMatrixSet(transform);
        }
     }

   //+------------------------------------------------------------------+
   //| Draw the full 2D distribution plot with axes and overlays       |
   //+------------------------------------------------------------------+
   void render2DVisualization()
     {
      //--- Clear canvas to transparent
      m_mainCanvas.Erase(0);
      //--- Fill the canvas body with the gradient background
      if(enableBackgroundFill)
         drawGradientBackground();
      //--- Draw the outer border frame
      drawCanvasBorder();
      //--- Draw the header bar with title and switch icon
      drawHeaderBar();
      //--- Draw the histogram and PMF curve in the plot area
      drawDistributionPlot();
      //--- Overlay statistics and legend panels if enabled
      if(showStatistics)
        {
         drawStatisticsPanel();
         drawLegend();
        }
      //--- Draw the resize grip indicator when hovering
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicator();
      //--- Flush the pixel buffer to the chart object
      m_mainCanvas.Update();
     }

   //+------------------------------------------------------------------+
   //| Render the 3D scene and overlay 2D UI elements on top           |
   //+------------------------------------------------------------------+
   void render3DVisualization()
     {
      //--- Abort if data has not been loaded
      if(!m_isDataLoaded) return;

      //--- Recompute the view and light transforms for this frame
      updateCameraPosition();
      //--- Reposition all 3D histogram bars to match current data
      update3DHistogramBars();

      //--- Use the configured background colour for the clear pass
      color bgColor     = backgroundTopColor;
      uint  bgColorArgb = ColorToARGB(bgColor, 255);
      //--- Clear colour and depth buffers, then render the 3D scene
      m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

      //--- Overlay the 2D border frame on the 3D render
      if(showBorderFrame)
         draw3DBorder();
      //--- Overlay the header bar on the rendered 3D image
      drawHeaderBarOn3D();
      //--- Overlay the stats panel and legend if enabled
      if(showStatistics)
        {
         drawStatisticsPanelOn3D();
         drawLegendOn3D();
        }
      //--- Reposition all PMF curve tube segments for this frame
      update3DCurveSegments();
      //--- Draw the pan mode toggle icon
      drawPanIconOverlay();
      //--- Draw the interactive view cube widget
      drawViewCubeOverlay();
      //--- Draw the resize grip indicator when hovering
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicatorOn3D();
      //--- Flush the pixel buffer to the chart object
      m_mainCanvas.Update();
     }

   //+------------------------------------------------------------------+
   //| Draw the pan mode toggle button overlaid on the 3D canvas       |
   //+------------------------------------------------------------------+
   void drawPanIconOverlay()
     {
      //--- Pan icon is only drawn in 3D mode
      if(m_currentViewMode != VIEW_3D_MODE) return;

      //--- Compute icon position below the header on the right side
      int iconX = m_currentWidth  - PAN_ICON_SIZE - PAN_ICON_MARGIN;
      int iconY = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
      int cx    = iconX + PAN_ICON_SIZE / 2;
      int cy    = iconY + PAN_ICON_SIZE / 2;
      int rad   = PAN_ICON_SIZE / 2;

      //--- Compute icon background colour based on interaction state
      color iconBgColor;
      if(m_panMode)
         iconBgColor = clrGreen;                         // Active pan mode: green
      else if(m_isHoveringPanIcon)
         iconBgColor = DarkenColor(themeColor, 0.1);     // Hover: slightly darker
      else
         iconBgColor = LightenColor(themeColor, 0.5);    // Default: lighter shade

      uint argbBg     = ColorToARGB(iconBgColor, 220);
      uint argbBorder = ColorToARGB(themeColor, 255);
      uint argbWhite  = ColorToARGB(clrWhite, 255);

      //--- Fill the circular icon background
      for(int py = cy - rad; py <= cy + rad; py++)
         for(int px = cx - rad; px <= cx + rad; px++)
           {
            int dx = px - cx;
            int dy = py - cy;
            if(dx * dx + dy * dy <= rad * rad)
               blendPixelSet(m_mainCanvas, px, py, argbBg);
           }

      //--- Draw the circular icon border ring
      for(int py = cy - rad; py <= cy + rad; py++)
         for(int px = cx - rad; px <= cx + rad; px++)
           {
            int dx = px - cx;
            int dy = py - cy;
            int distSq = dx * dx + dy * dy;
            if(distSq <= rad * rad && distSq >= (rad - 1) * (rad - 1))
               blendPixelSet(m_mainCanvas, px, py, argbBorder);
           }

      //--- Draw the four-arrow pan icon symbol in white
      int arrLen  = 4;
      int arrHead = 2;
      //--- Draw the horizontal crosshair bar
      for(int i = -arrLen; i <= arrLen; i++)
         blendPixelSet(m_mainCanvas, cx + i, cy, argbWhite);
      //--- Draw the vertical crosshair bar
      for(int i = -arrLen; i <= arrLen; i++)
         blendPixelSet(m_mainCanvas, cx, cy + i, argbWhite);
      //--- Draw the four arrowheads at each end of the crosshair
      for(int i = 0; i <= arrHead; i++)
        {
         blendPixelSet(m_mainCanvas, cx + arrLen - i, cy - i, argbWhite); // Right arrow top
         blendPixelSet(m_mainCanvas, cx + arrLen - i, cy + i, argbWhite); // Right arrow bottom
         blendPixelSet(m_mainCanvas, cx - arrLen + i, cy - i, argbWhite); // Left arrow top
         blendPixelSet(m_mainCanvas, cx - arrLen + i, cy + i, argbWhite); // Left arrow bottom
         blendPixelSet(m_mainCanvas, cx - i, cy - arrLen + i, argbWhite); // Up arrow left
         blendPixelSet(m_mainCanvas, cx + i, cy - arrLen + i, argbWhite); // Up arrow right
         blendPixelSet(m_mainCanvas, cx - i, cy + arrLen - i, argbWhite); // Down arrow left
         blendPixelSet(m_mainCanvas, cx + i, cy + arrLen - i, argbWhite); // Down arrow right
        }
     }

   //+------------------------------------------------------------------+
   //| Project a 3D view cube vertex into 2D screen coordinates        |
   //+------------------------------------------------------------------+
   void vcubeProject(double x, double y, double z, double angX, double angY, int &sx, int &sy)
     {
      //--- Pre-compute trigonometric values for both rotation angles
      double cosAX = MathCos(angX), sinAX = MathSin(angX);
      double cosAY = MathCos(angY), sinAY = MathSin(angY);
      //--- Rotate the point around the X axis (elevation)
      double y2 = y * cosAX - z * sinAX;
      double z2 = y * sinAX + z * cosAX;
      //--- Rotate the result around the Y axis (azimuth)
      double x2 = x * cosAY + z2 * sinAY;
      //--- Apply a fixed orthographic scale relative to the cube widget size
      double scale = VCUBE_SIZE * 0.30;
      //--- Map to screen pixels relative to the view cube centre
      sx = m_vcubeCenterX + (int)(x2 * scale);
      sy = m_vcubeCenterY - (int)(y2 * scale);
     }

   //+------------------------------------------------------------------+
   //| Draw and label the interactive 3D view cube overlay widget       |
   //+------------------------------------------------------------------+
   void drawViewCubeOverlay()
     {
      //--- View cube is only drawn in 3D mode
      if(m_currentViewMode != VIEW_3D_MODE) return;

      //--- Compute the view cube bounding area below the pan icon
      int areaX       = m_currentWidth  - VCUBE_SIZE - VCUBE_MARGIN;
      int areaY       = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
      m_vcubeCenterX  = areaX + VCUBE_SIZE / 2;
      m_vcubeCenterY  = areaY + VCUBE_SIZE / 2;

      //--- Optionally draw a semi-transparent background panel behind the cube
      if(showViewCubeBackground)
        {
         uint argbPanelBg     = ColorToARGB(LightenColor(themeColor, 0.92), 200);
         uint argbPanelBorder = ColorToARGB(themeColor, 200);
         //--- Fill the background panel
         for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
            for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
               blendPixelSet(m_mainCanvas, px, py, argbPanelBg);
         //--- Draw top and bottom borders
         for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
           {
            blendPixelSet(m_mainCanvas, px, areaY, argbPanelBorder);
            blendPixelSet(m_mainCanvas, px, areaY + VCUBE_SIZE - 1, argbPanelBorder);
           }
         //--- Draw left and right borders
         for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
           {
            blendPixelSet(m_mainCanvas, areaX, py, argbPanelBorder);
            blendPixelSet(m_mainCanvas, areaX + VCUBE_SIZE - 1, py, argbPanelBorder);
           }
        }

      //--- Use the current camera angles as the cube orientation
      double angX = m_cameraAngleX;
      double angY = m_cameraAngleY;

      //--- Define the 8 vertices of the unit cube in local space
      double verts[8][3];
      verts[0][0] = -1; verts[0][1] = -1; verts[0][2] = -1;
      verts[1][0] =  1; verts[1][1] = -1; verts[1][2] = -1;
      verts[2][0] =  1; verts[2][1] =  1; verts[2][2] = -1;
      verts[3][0] = -1; verts[3][1] =  1; verts[3][2] = -1;
      verts[4][0] = -1; verts[4][1] = -1; verts[4][2] =  1;
      verts[5][0] =  1; verts[5][1] = -1; verts[5][2] =  1;
      verts[6][0] =  1; verts[6][1] =  1; verts[6][2] =  1;
      verts[7][0] = -1; verts[7][1] =  1; verts[7][2] =  1;

      //--- Project all 8 vertices to screen space
      int sv[8][2];
      for(int i = 0; i < 8; i++)
         vcubeProject(verts[i][0], verts[i][1], verts[i][2], angX, angY, sv[i][0], sv[i][1]);

      //--- Define the 6 faces by their vertex indices (counter-clockwise)
      int faces[6][4];
      faces[0][0]=0; faces[0][1]=1; faces[0][2]=2; faces[0][3]=3; // Front
      faces[1][0]=5; faces[1][1]=4; faces[1][2]=7; faces[1][3]=6; // Back
      faces[2][0]=3; faces[2][1]=2; faces[2][2]=6; faces[2][3]=7; // Top
      faces[3][0]=0; faces[3][1]=1; faces[3][2]=5; faces[3][3]=4; // Bottom
      faces[4][0]=1; faces[4][1]=5; faces[4][2]=6; faces[4][3]=2; // Right
      faces[5][0]=4; faces[5][1]=0; faces[5][2]=3; faces[5][3]=7; // Left

      //--- Face label strings for text overlay
      string faceNames[6];
      faceNames[0] = "Front";
      faceNames[1] = "Back";
      faceNames[2] = "Top";
      faceNames[3] = "Bottom";
      faceNames[4] = "Right";
      faceNames[5] = "Left";

      //--- Face outward normals for back-face culling
      double faceNormals[6][3];
      faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
      faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
      faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
      faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
      faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
      faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

      //--- Camera direction vector for depth sorting and back-face culling
      double camDir[3];
      camDir[0] =  MathCos(angX) * MathSin(angY);
      camDir[1] = -MathSin(angX);
      camDir[2] = -MathCos(angX) * MathCos(angY);

      //--- Sort faces back-to-front using their projected depth (painter's algorithm)
      int    faceOrder[6];
      double faceDepth[6];
      for(int i = 0; i < 6; i++)
        {
         faceOrder[i] = i;
         double cx = 0, cy2 = 0, cz = 0;
         //--- Compute the face centre as the average of its four vertices
         for(int j = 0; j < 4; j++)
           {
            cx  += verts[faces[i][j]][0];
            cy2 += verts[faces[i][j]][1];
            cz  += verts[faces[i][j]][2];
           }
         cx /= 4.0; cy2 /= 4.0; cz /= 4.0;
         //--- Project the centre onto the camera direction for depth
         faceDepth[i] = cx * camDir[0] + cy2 * camDir[1] + cz * camDir[2];
        }
      //--- Bubble-sort the face order from farthest to nearest
      for(int i = 0; i < 5; i++)
         for(int j = i + 1; j < 6; j++)
            if(faceDepth[faceOrder[i]] > faceDepth[faceOrder[j]])
              {
               int tmp = faceOrder[i];
               faceOrder[i] = faceOrder[j];
               faceOrder[j] = tmp;
              }

      //--- Assign a distinct base colour to each face for identification
      color faceColors[6];
      faceColors[0] = clrCornflowerBlue; // Front
      faceColors[1] = clrSteelBlue;      // Back
      faceColors[2] = clrLimeGreen;      // Top
      faceColors[3] = clrDarkGreen;      // Bottom
      faceColors[4] = clrOrangeRed;      // Right
      faceColors[5] = clrDarkOrange;     // Left

      //--- Sub-face zone names for all 6 faces (3x3 grid per face)
      string faceSubNames[6][3][3];
      // Front face sub-zones
      faceSubNames[0][0][0] = "BottomFrontLeft";   faceSubNames[0][1][0] = "BottomFront";   faceSubNames[0][2][0] = "BottomFrontRight";
      faceSubNames[0][0][1] = "FrontLeft";          faceSubNames[0][1][1] = "Front";         faceSubNames[0][2][1] = "FrontRight";
      faceSubNames[0][0][2] = "TopFrontLeft";       faceSubNames[0][1][2] = "TopFront";      faceSubNames[0][2][2] = "TopFrontRight";
      // Back face sub-zones
      faceSubNames[1][0][0] = "BottomBackLeft";     faceSubNames[1][1][0] = "BottomBack";    faceSubNames[1][2][0] = "BottomBackRight";
      faceSubNames[1][0][1] = "BackLeft";           faceSubNames[1][1][1] = "Back";          faceSubNames[1][2][1] = "BackRight";
      faceSubNames[1][0][2] = "TopBackLeft";        faceSubNames[1][1][2] = "TopBack";       faceSubNames[1][2][2] = "TopBackRight";
      // Top face sub-zones
      faceSubNames[2][0][0] = "TopFrontLeft";       faceSubNames[2][1][0] = "TopFront";      faceSubNames[2][2][0] = "TopFrontRight";
      faceSubNames[2][0][1] = "TopLeft";            faceSubNames[2][1][1] = "Top";           faceSubNames[2][2][1] = "TopRight";
      faceSubNames[2][0][2] = "TopBackLeft";        faceSubNames[2][1][2] = "TopBack";       faceSubNames[2][2][2] = "TopBackRight";
      // Bottom face sub-zones
      faceSubNames[3][0][0] = "BottomFrontLeft";   faceSubNames[3][1][0] = "BottomFront";   faceSubNames[3][2][0] = "BottomFrontRight";
      faceSubNames[3][0][1] = "BottomLeft";         faceSubNames[3][1][1] = "Bottom";        faceSubNames[3][2][1] = "BottomRight";
      faceSubNames[3][0][2] = "BottomBackLeft";     faceSubNames[3][1][2] = "BottomBack";    faceSubNames[3][2][2] = "BottomBackRight";
      // Right face sub-zones
      faceSubNames[4][0][0] = "BottomFrontRight";  faceSubNames[4][1][0] = "BottomRight";   faceSubNames[4][2][0] = "BottomBackRight";
      faceSubNames[4][0][1] = "FrontRight";         faceSubNames[4][1][1] = "Right";         faceSubNames[4][2][1] = "BackRight";
      faceSubNames[4][0][2] = "TopFrontRight";      faceSubNames[4][1][2] = "TopRight";      faceSubNames[4][2][2] = "TopBackRight";
      // Left face sub-zones
      faceSubNames[5][0][0] = "BottomFrontLeft";   faceSubNames[5][1][0] = "BottomLeft";    faceSubNames[5][2][0] = "BottomBackLeft";
      faceSubNames[5][0][1] = "FrontLeft";          faceSubNames[5][1][1] = "Left";          faceSubNames[5][2][1] = "BackLeft";
      faceSubNames[5][0][2] = "TopFrontLeft";       faceSubNames[5][1][2] = "TopLeft";       faceSubNames[5][2][2] = "TopBackLeft";

      //--- Shrink factor creates a small visible gap between sub-face tiles
      double shrink = 0.03;

      //--- Render visible faces in back-to-front order
      for(int fi = 0; fi < 6; fi++)
        {
         int f = faceOrder[fi];
         //--- Compute the dot product to cull back-facing faces
         double dot = faceNormals[f][0] * camDir[0] +
                      faceNormals[f][1] * camDir[1] +
                      faceNormals[f][2] * camDir[2];
         if(dot >= 0) continue;

         //--- Compute Lambert diffuse brightness from the dot product
         double brightness = MathAbs(dot);
         brightness = 0.4 + 0.6 * brightness;

         //--- Apply brightness to the base face colour
         color fc = faceColors[f];
         uchar fr_base = (uchar)MathMin(255, (int)(((fc >> 16) & 0xFF) * brightness));
         uchar fg_base = (uchar)MathMin(255, (int)(((fc >> 8)  & 0xFF) * brightness));
         uchar fb_base = (uchar)MathMin(255, (int)(( fc        & 0xFF) * brightness));

         uchar alpha = 180;

         //--- Get the four projected screen vertices for this face
         int fx[4], fy[4];
         for(int j = 0; j < 4; j++)
           {
            fx[j] = sv[faces[f][j]][0];
            fy[j] = sv[faces[f][j]][1];
           }

         //--- Draw each 3x3 sub-face tile using bilinear interpolation
         for(int i = 0; i < 3; i++)
            for(int j = 0; j < 3; j++)
              {
               //--- Compute the UV bounds for this sub-tile with shrink margin
               double u1 = i / 3.0 + shrink;
               double u2 = (i + 1) / 3.0 - shrink;
               double v1 = j / 3.0 + shrink;
               double v2 = (j + 1) / 3.0 - shrink;
               if(u1 >= u2 || v1 >= v2) continue;

               //--- Compute the four screen-space corners of this sub-tile
               int sub_fx[4], sub_fy[4];
               sub_fx[0] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v1);
               sub_fy[0] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v1);
               sub_fx[1] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v1);
               sub_fy[1] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v1);
               sub_fx[2] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v2);
               sub_fy[2] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v2);
               sub_fx[3] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v2);
               sub_fy[3] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v2);

               //--- Look up the zone name for this sub-tile
               string subZone  = faceSubNames[f][i][j];
               //--- Check if this tile is the one currently hovered
               bool isHovered = (m_vcubeHoverZone == subZone);

               //--- Copy base colour channels for potential brightening
               uchar fr = fr_base;
               uchar fg = fg_base;
               uchar fb = fb_base;
               //--- Increase alpha and brighten colour when hovered
               uchar subAlpha = isHovered ? (uchar)240 : alpha;
               if(isHovered)
                 {
                  fr = (uchar)MathMin(255, fr + 40);
                  fg = (uchar)MathMin(255, fg + 40);
                  fb = (uchar)MathMin(255, fb + 40);
                 }
               //--- Pack ARGB colour for this sub-tile
               uint argbFace = ((uint)subAlpha << 24) | ((uint)fr << 16) | ((uint)fg << 8) | (uint)fb;
               //--- Fill the sub-tile quad with the computed colour
               fillQuad(sub_fx, sub_fy, argbFace);
              }

         //--- Draw the four black outline edges of the face
         uint argbEdge = ColorToARGB(clrBlack, 200);
         for(int j = 0; j < 4; j++)
           {
            int next = (j + 1) % 4;
            m_mainCanvas.LineAA(fx[j], fy[j], fx[next], fy[next], argbEdge);
           }

         //--- Compute the face screen-space centre for text placement
         int fcx = (fx[0] + fx[1] + fx[2] + fx[3]) / 4;
         int fcy = (fy[0] + fy[1] + fy[2] + fy[3]) / 4;

         //--- Draw the face name label only for well-lit faces
         if(brightness > 0.5)
           {
            m_mainCanvas.FontSet("Arial Bold", 7);
            uint textCol = ColorToARGB(clrWhite, 220);
            m_mainCanvas.TextOut(fcx, fcy - 4, faceNames[f], textCol, TA_CENTER);
           }
        }

      //--- Draw the three coordinate axis indicators on the view cube
      uint argbAxis;
      int  ox, oy;
      vcubeProject(0, 0, 0, angX, angY, ox, oy);

      //--- Draw the X axis indicator and label
      int axEnd;
      argbAxis = ColorToARGB(clrRed, 220);
      vcubeProject(1.4, 0, 0, angX, angY, axEnd, oy);
      m_mainCanvas.LineAA(ox, oy, axEnd, oy, argbAxis);
      m_mainCanvas.FontSet("Arial Bold", 7);
      m_mainCanvas.TextOut(axEnd + 2, oy - 4, "X", ColorToARGB(clrRed, 255), TA_LEFT);

      //--- Draw the Y axis indicator and label
      int dummy;
      argbAxis = ColorToARGB(clrGreen, 220);
      vcubeProject(0, 1.4, 0, angX, angY, dummy, axEnd);
      m_mainCanvas.LineAA(ox, oy, dummy, axEnd, argbAxis);
      m_mainCanvas.TextOut(dummy + 2, axEnd - 4, "Y", ColorToARGB(clrGreen, 255), TA_LEFT);

      //--- Draw the Z axis indicator and label
      argbAxis = ColorToARGB(clrBlue, 220);
      vcubeProject(0, 0, 1.4, angX, angY, axEnd, dummy);
      m_mainCanvas.LineAA(ox, oy, axEnd, dummy, argbAxis);
      m_mainCanvas.TextOut(axEnd + 2, dummy - 4, "Z", ColorToARGB(clrBlue, 255), TA_LEFT);
     }

   //+------------------------------------------------------------------+
   //| Bilinearly interpolate between four corner values                |
   //+------------------------------------------------------------------+
   double bilinear(double p00, double p10, double p01, double p11, double u, double v)
     {
      //--- Compute the weighted blend of the four corner values
      return (1 - u) * (1 - v) * p00 + u * (1 - v) * p10 +
             (1 - u) *      v  * p01 + u *       v  * p11;
     }

   //+------------------------------------------------------------------+
   //| Scanline-fill a convex quadrilateral with the given ARGB colour  |
   //+------------------------------------------------------------------+
   void fillQuad(int &px[], int &py[], uint clr)
     {
      //--- Determine the vertical span of the quad
      int minY = py[0], maxY = py[0];
      for(int i = 1; i < 4; i++)
        {
         if(py[i] < minY) minY = py[i];
         if(py[i] > maxY) maxY = py[i];
        }

      //--- Scanline-fill the quad row by row
      for(int y = minY; y <= maxY; y++)
        {
         int minX = 99999, maxX = -99999;
         //--- Find the left and right intersection X for this scanline
         for(int i = 0; i < 4; i++)
           {
            int j  = (i + 1) % 4;
            int y1 = py[i], y2 = py[j];
            int x1 = px[i], x2 = px[j];
            //--- Process edges that cross this scanline
            if((y1 <= y && y2 >= y) || (y2 <= y && y1 >= y))
              {
               if(y1 == y2)
                 {
                  //--- Horizontal edge: include both endpoints
                  if(x1 < minX) minX = x1;
                  if(x2 < minX) minX = x2;
                  if(x1 > maxX) maxX = x1;
                  if(x2 > maxX) maxX = x2;
                 }
               else
                 {
                  //--- Non-horizontal edge: interpolate the X intersection
                  int ix = x1 + (y - y1) * (x2 - x1) / (y2 - y1);
                  if(ix < minX) minX = ix;
                  if(ix > maxX) maxX = ix;
                 }
              }
           }
         //--- Paint every pixel on this scanline row
         for(int x = minX; x <= maxX; x++)
            blendPixelSet(m_mainCanvas, x, y, clr);
        }
     }

   //+------------------------------------------------------------------+
   //| Detect which face, edge or corner of the view cube is hovered    |
   //+------------------------------------------------------------------+
   void detectViewCubeZone(int localX, int localY)
     {
      //--- Use the current camera angles for the cube orientation
      double angX = m_cameraAngleX;
      double angY = m_cameraAngleY;

      //--- Compute the camera direction for back-face filtering
      double camDir[3];
      camDir[0] =  MathCos(angX) * MathSin(angY);
      camDir[1] = -MathSin(angX);
      camDir[2] = -MathCos(angX) * MathCos(angY);

      //--- Face name and normal lookup tables
      string faceNames[6];
      faceNames[0] = "Front"; faceNames[1] = "Back"; faceNames[2] = "Top";
      faceNames[3] = "Bottom"; faceNames[4] = "Right"; faceNames[5] = "Left";

      double faceNormals[6][3];
      faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
      faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
      faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
      faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
      faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
      faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

      //--- Face centre lookup table
      double faceCenters[6][3];
      faceCenters[0][0]= 0; faceCenters[0][1]= 0; faceCenters[0][2]=-1;
      faceCenters[1][0]= 0; faceCenters[1][1]= 0; faceCenters[1][2]= 1;
      faceCenters[2][0]= 0; faceCenters[2][1]= 1; faceCenters[2][2]= 0;
      faceCenters[3][0]= 0; faceCenters[3][1]=-1; faceCenters[3][2]= 0;
      faceCenters[4][0]= 1; faceCenters[4][1]= 0; faceCenters[4][2]= 0;
      faceCenters[5][0]=-1; faceCenters[5][1]= 0; faceCenters[5][2]= 0;

      //--- Initialise the best match distance and clear the zone
      double bestDist = 99999;
      m_vcubeHoverZone = "";

      //--- Check each visible face centre against the cursor position
      for(int i = 0; i < 6; i++)
        {
         //--- Skip back-facing faces (they cannot be clicked)
         double dot = faceNormals[i][0] * camDir[0] +
                      faceNormals[i][1] * camDir[1] +
                      faceNormals[i][2] * camDir[2];
         if(dot >= 0) continue;
         int sx, sy;
         vcubeProject(faceCenters[i][0], faceCenters[i][1], faceCenters[i][2], angX, angY, sx, sy);
         double dx = localX - sx;
         double dy = localY - sy;
         double d  = MathSqrt(dx * dx + dy * dy);
         //--- Update the closest zone within a 15-pixel radius
         if(d < 15 && d < bestDist)
           {
            bestDist = d;
            m_vcubeHoverZone = faceNames[i];
           }
        }

      //--- Edge midpoint lookup tables
      double edgeCenters[12][3];
      string edgeNames[12];
      edgeCenters[0][0]= 0;  edgeCenters[0][1]= 1;  edgeCenters[0][2]=-1;  edgeNames[0]  = "TopFront";
      edgeCenters[1][0]= 0;  edgeCenters[1][1]= 1;  edgeCenters[1][2]= 1;  edgeNames[1]  = "TopBack";
      edgeCenters[2][0]= 1;  edgeCenters[2][1]= 1;  edgeCenters[2][2]= 0;  edgeNames[2]  = "TopRight";
      edgeCenters[3][0]=-1;  edgeCenters[3][1]= 1;  edgeCenters[3][2]= 0;  edgeNames[3]  = "TopLeft";
      edgeCenters[4][0]= 0;  edgeCenters[4][1]=-1;  edgeCenters[4][2]=-1;  edgeNames[4]  = "BottomFront";
      edgeCenters[5][0]= 0;  edgeCenters[5][1]=-1;  edgeCenters[5][2]= 1;  edgeNames[5]  = "BottomBack";
      edgeCenters[6][0]= 1;  edgeCenters[6][1]=-1;  edgeCenters[6][2]= 0;  edgeNames[6]  = "BottomRight";
      edgeCenters[7][0]=-1;  edgeCenters[7][1]=-1;  edgeCenters[7][2]= 0;  edgeNames[7]  = "BottomLeft";
      edgeCenters[8][0]= 1;  edgeCenters[8][1]= 0;  edgeCenters[8][2]=-1;  edgeNames[8]  = "FrontRight";
      edgeCenters[9][0]=-1;  edgeCenters[9][1]= 0;  edgeCenters[9][2]=-1;  edgeNames[9]  = "FrontLeft";
      edgeCenters[10][0]= 1; edgeCenters[10][1]= 0; edgeCenters[10][2]= 1; edgeNames[10] = "BackRight";
      edgeCenters[11][0]=-1; edgeCenters[11][1]= 0; edgeCenters[11][2]= 1; edgeNames[11] = "BackLeft";

      //--- Check each edge midpoint against the cursor position (10-pixel radius)
      for(int i = 0; i < 12; i++)
        {
         int sx, sy;
         vcubeProject(edgeCenters[i][0], edgeCenters[i][1], edgeCenters[i][2], angX, angY, sx, sy);
         double dx = localX - sx;
         double dy = localY - sy;
         double d  = MathSqrt(dx * dx + dy * dy);
         if(d < 10 && d < bestDist)
           {
            bestDist = d;
            m_vcubeHoverZone = edgeNames[i];
           }
        }

      //--- Corner position and name lookup tables
      double cornerCenters[8][3];
      string cornerNames[8];
      cornerCenters[0][0]=-1; cornerCenters[0][1]= 1; cornerCenters[0][2]=-1; cornerNames[0]="TopFrontLeft";
      cornerCenters[1][0]= 1; cornerCenters[1][1]= 1; cornerCenters[1][2]=-1; cornerNames[1]="TopFrontRight";
      cornerCenters[2][0]= 1; cornerCenters[2][1]= 1; cornerCenters[2][2]= 1; cornerNames[2]="TopBackRight";
      cornerCenters[3][0]=-1; cornerCenters[3][1]= 1; cornerCenters[3][2]= 1; cornerNames[3]="TopBackLeft";
      cornerCenters[4][0]=-1; cornerCenters[4][1]=-1; cornerCenters[4][2]=-1; cornerNames[4]="BottomFrontLeft";
      cornerCenters[5][0]= 1; cornerCenters[5][1]=-1; cornerCenters[5][2]=-1; cornerNames[5]="BottomFrontRight";
      cornerCenters[6][0]= 1; cornerCenters[6][1]=-1; cornerCenters[6][2]= 1; cornerNames[6]="BottomBackRight";
      cornerCenters[7][0]=-1; cornerCenters[7][1]=-1; cornerCenters[7][2]= 1; cornerNames[7]="BottomBackLeft";

      //--- Check each corner against the cursor position (8-pixel radius)
      for(int i = 0; i < 8; i++)
        {
         int sx, sy;
         vcubeProject(cornerCenters[i][0], cornerCenters[i][1], cornerCenters[i][2], angX, angY, sx, sy);
         double dx = localX - sx;
         double dy = localY - sy;
         double d  = MathSqrt(dx * dx + dy * dy);
         if(d < 8 && d < bestDist)
           {
            bestDist = d;
            m_vcubeHoverZone = cornerNames[i];
           }
        }
     }

   //+------------------------------------------------------------------+
   //| Snap the camera to the orientation indicated by a cube zone click|
   //+------------------------------------------------------------------+
   void handleViewCubeClick(int mouseX, int mouseY)
     {
      //--- Abort if no valid zone is hovered
      if(m_vcubeHoverZone == "") return;

      //--- Diagonal tilt angle used for isometric corner/edge views
      double isoTilt = MathArctan(1.0 / MathSqrt(2.0));
      //--- Near-vertical angle used for top/bottom face views
      double nearPole = DX_PI / 2.0 - 0.0001;

      //--- Look up the target camera angles for the clicked zone
      double tX = 0, tY = 0;
      if     (m_vcubeHoverZone == "Front")              { tX = 0.0;        tY = 0.0; }
      else if(m_vcubeHoverZone == "Back")               { tX = 0.0;        tY = DX_PI; }
      else if(m_vcubeHoverZone == "Top")                { tX = nearPole;   tY = 0.0; }
      else if(m_vcubeHoverZone == "Bottom")             { tX = -nearPole;  tY = 0.0; }
      else if(m_vcubeHoverZone == "Right")              { tX = 0.0;        tY =  DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "Left")               { tX = 0.0;        tY = -DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "TopFront")           { tX = isoTilt;    tY = 0.0; }
      else if(m_vcubeHoverZone == "TopBack")            { tX = isoTilt;    tY = DX_PI; }
      else if(m_vcubeHoverZone == "TopRight")           { tX = isoTilt;    tY =  DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "TopLeft")            { tX = isoTilt;    tY = -DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "BottomFront")        { tX = -isoTilt;   tY = 0.0; }
      else if(m_vcubeHoverZone == "BottomBack")         { tX = -isoTilt;   tY = DX_PI; }
      else if(m_vcubeHoverZone == "BottomRight")        { tX = -isoTilt;   tY =  DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "BottomLeft")         { tX = -isoTilt;   tY = -DX_PI / 2.0; }
      else if(m_vcubeHoverZone == "FrontRight")         { tX = 0.0;        tY =  DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "FrontLeft")          { tX = 0.0;        tY = -DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "BackRight")          { tX = 0.0;        tY =  DX_PI * 3.0 / 4.0; }
      else if(m_vcubeHoverZone == "BackLeft")           { tX = 0.0;        tY = -DX_PI * 3.0 / 4.0; }
      else if(m_vcubeHoverZone == "TopFrontRight")      { tX = isoTilt;    tY =  DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "TopFrontLeft")       { tX = isoTilt;    tY = -DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "TopBackRight")       { tX = isoTilt;    tY =  DX_PI * 3.0 / 4.0; }
      else if(m_vcubeHoverZone == "TopBackLeft")        { tX = isoTilt;    tY = -DX_PI * 3.0 / 4.0; }
      else if(m_vcubeHoverZone == "BottomFrontRight")   { tX = -isoTilt;   tY =  DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "BottomFrontLeft")    { tX = -isoTilt;   tY = -DX_PI / 4.0; }
      else if(m_vcubeHoverZone == "BottomBackRight")    { tX = -isoTilt;   tY =  DX_PI * 3.0 / 4.0; }
      else if(m_vcubeHoverZone == "BottomBackLeft")     { tX = -isoTilt;   tY = -DX_PI * 3.0 / 4.0; }
      else return;

      //--- Store the target angles for the animation
      m_targetAngleX    = tX;
      m_targetAngleY    = tY;
      //--- Record the current angles as the animation start
      m_animStartAngleX = m_cameraAngleX;
      m_animStartAngleY = m_cameraAngleY;
      //--- Reset and start the animation
      m_animStep        = 0;
      m_animSteps       = 20;
      m_isAnimating     = true;
      //--- Start the millisecond timer to drive the animation
      EventSetMillisecondTimer(30);
     }

   //+------------------------------------------------------------------+
   //| Bin sample data into a fixed-cell frequency histogram           |
   //+------------------------------------------------------------------+
   bool computeHistogram(const double &data[], double &intervals[], double &frequency[],
                         double &maxv, double &minv, const int cells = 10)
     {
      //--- Reject degenerate cell count
      if(cells <= 1) return false;
      //--- Get the number of data points
      int size = ArraySize(data);
      if(size < 1) return false;

      //--- Force the histogram range to cover the full trial space
      minv = 0;
      maxv = numTrials;
      double range = maxv - minv;
      double width = range / cells;
      if(width == 0) return false;

      //--- Allocate the bin centre and frequency buffers
      ArrayResize(intervals, cells);
      ArrayResize(frequency, cells);

      //--- Initialise each bin centre and zero its counter
      for(int i = 0; i < cells; i++)
        {
         intervals[i] = minv + (i + 0.5) * width;
         frequency[i] = 0;
        }

      //--- Classify each sample value into its bin
      for(int i = 0; i < size; i++)
        {
         double val = data[i];
         if(val < minv || val > maxv) continue;
         int ind = (int)((val - minv) / width);
         if(ind >= cells) ind = cells - 1;
         if(ind < 0) ind = 0;
         frequency[ind]++;
        }
      return true;
     }

   //+------------------------------------------------------------------+
   //| Compute all descriptive statistics from the loaded sample        |
   //+------------------------------------------------------------------+
   void computeAdvancedStatistics()
     {
      //--- Compute the arithmetic mean of the sample
      m_sampleMean = calculateMean(m_sampleData);
      //--- Compute the corrected sample standard deviation
      m_sampleStandardDeviation = calculateStandardDeviation(m_sampleData, m_sampleMean);
      //--- Compute the Fisher-adjusted skewness
      m_sampleSkewness = calculateSkewness(m_sampleData, m_sampleMean, m_sampleStandardDeviation);
      //--- Compute the excess kurtosis (Fisher definition)
      m_sampleKurtosis = calculateKurtosis(m_sampleData, m_sampleMean, m_sampleStandardDeviation);
      //--- Compute the first quartile
      m_percentile25 = calculatePercentile(m_sampleData, 25.0);
      //--- Compute the median
      m_percentile50 = calculatePercentile(m_sampleData, 50.0);
      //--- Compute the third quartile
      m_percentile75 = calculatePercentile(m_sampleData, 75.0);
      //--- Compute the 95% confidence interval using z = 1.96
      calculateConfidenceInterval(m_sampleMean, m_sampleStandardDeviation, sampleSize, 1.96,
                                  m_confidenceInterval95Lower, m_confidenceInterval95Upper);
      //--- Compute the 99% confidence interval using z = 2.576
      calculateConfidenceInterval(m_sampleMean, m_sampleStandardDeviation, sampleSize, 2.576,
                                  m_confidenceInterval99Lower, m_confidenceInterval99Upper);
     }

   //+------------------------------------------------------------------+
   //| Compute and return the arithmetic mean of a data array           |
   //+------------------------------------------------------------------+
   double calculateMean(const double &data[])
     {
      int size = ArraySize(data);
      if(size == 0) return 0.0;
      double sum = 0.0;
      for(int i = 0; i < size; i++)
         sum += data[i];
      return sum / size;
     }

   //+------------------------------------------------------------------+
   //| Compute and return the corrected sample standard deviation       |
   //+------------------------------------------------------------------+
   double calculateStandardDeviation(const double &data[], double mean)
     {
      int size = ArraySize(data);
      if(size <= 1) return 0.0;
      double sumSquaredDiff = 0.0;
      for(int i = 0; i < size; i++)
        {
         double diff = data[i] - mean;
         sumSquaredDiff += diff * diff;
        }
      return MathSqrt(sumSquaredDiff / (size - 1));
     }

   //+------------------------------------------------------------------+
   //| Compute and return the Fisher-adjusted sample skewness           |
   //+------------------------------------------------------------------+
   double calculateSkewness(const double &data[], double mean, double stdDev)
     {
      int size = ArraySize(data);
      if(size < 3 || stdDev == 0.0) return 0.0;
      double sumCubedDiff = 0.0;
      for(int i = 0; i < size; i++)
        {
         double diff = (data[i] - mean) / stdDev;
         sumCubedDiff += diff * diff * diff;
        }
      double n = (double)size;
      return (n / ((n - 1) * (n - 2))) * sumCubedDiff;
     }

   //+------------------------------------------------------------------+
   //| Compute and return the Fisher excess sample kurtosis             |
   //+------------------------------------------------------------------+
   double calculateKurtosis(const double &data[], double mean, double stdDev)
     {
      int size = ArraySize(data);
      if(size < 4 || stdDev == 0.0) return 0.0;
      double sumFourthPower = 0.0;
      for(int i = 0; i < size; i++)
        {
         double diff    = (data[i] - mean) / stdDev;
         double squared = diff * diff;
         sumFourthPower += squared * squared;
        }
      double n        = (double)size;
      double kurtosis = (n * (n + 1) / ((n - 1) * (n - 2) * (n - 3))) * sumFourthPower;
      kurtosis -= (3 * (n - 1) * (n - 1)) / ((n - 2) * (n - 3));
      return kurtosis;
     }

   //+------------------------------------------------------------------+
   //| Compute and return a percentile using linear interpolation       |
   //+------------------------------------------------------------------+
   double calculatePercentile(double &data[], double percentile)
     {
      int size = ArraySize(data);
      if(size == 0) return 0.0;
      double sortedData[];
      ArrayResize(sortedData, size);
      ArrayCopy(sortedData, data);
      ArraySort(sortedData);
      double rank       = (percentile / 100.0) * (size - 1);
      int    lowerIndex = (int)MathFloor(rank);
      int    upperIndex = (int)MathCeil(rank);
      if(lowerIndex == upperIndex)
         return sortedData[lowerIndex];
      double fraction = rank - lowerIndex;
      return sortedData[lowerIndex] + fraction * (sortedData[upperIndex] - sortedData[lowerIndex]);
     }

   //+------------------------------------------------------------------+
   //| Compute a symmetric confidence interval around the mean          |
   //+------------------------------------------------------------------+
   void calculateConfidenceInterval(double mean, double stdDev, int in_sampleSize, double zScore,
                                    double &lowerBound, double &upperBound)
     {
      //--- Compute the half-width of the interval
      double marginOfError = zScore * (stdDev / MathSqrt(in_sampleSize));
      lowerBound = mean - marginOfError;
      upperBound = mean + marginOfError;
     }

   //+------------------------------------------------------------------+
   //| Return true when the cursor is over the header bar region        |
   //+------------------------------------------------------------------+
   bool isMouseOverHeaderBar(int mouseX, int mouseY)
     {
      return (mouseX >= m_currentPositionX &&
              mouseX <= m_currentPositionX + m_currentWidth &&
              mouseY >= m_currentPositionY &&
              mouseY <= m_currentPositionY + HEADER_BAR_HEIGHT);
     }

   //+------------------------------------------------------------------+
   //| Detect whether the cursor is in a resize grip zone              |
   //+------------------------------------------------------------------+
   bool isMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode)
     {
      //--- Return immediately if resizing is disabled
      if(!enableResizing) return false;

      //--- Convert absolute cursor position to canvas-relative coordinates
      int relativeX = mouseX - m_currentPositionX;
      int relativeY = mouseY - m_currentPositionY;

      //--- Test proximity to the right edge (excluding header)
      bool nearRightEdge  = (relativeX >= m_currentWidth  - resizeGripSize &&
                             relativeX <= m_currentWidth  &&
                             relativeY >= HEADER_BAR_HEIGHT &&
                             relativeY <= m_currentHeight);
      //--- Test proximity to the bottom edge
      bool nearBottomEdge = (relativeY >= m_currentHeight - resizeGripSize &&
                             relativeY <= m_currentHeight &&
                             relativeX >= 0 &&
                             relativeX <= m_currentWidth);
      //--- Test proximity to the bottom-right corner (takes priority)
      bool nearCorner     = (relativeX >= m_currentWidth  - resizeGripSize &&
                             relativeX <= m_currentWidth  &&
                             relativeY >= m_currentHeight - resizeGripSize &&
                             relativeY <= m_currentHeight);

      //--- Assign mode: corner has highest priority, then edges
      if(nearCorner)       { resizeMode = RESIZE_CORNER;      return true; }
      else if(nearRightEdge)  { resizeMode = RESIZE_RIGHT_EDGE;  return true; }
      else if(nearBottomEdge) { resizeMode = RESIZE_BOTTOM_EDGE; return true; }

      resizeMode = NO_RESIZE;
      return false;
     }

   //+------------------------------------------------------------------+
   //| Move the canvas by updating its chart object position            |
   //+------------------------------------------------------------------+
   void handleCanvasDrag(int mouseX, int mouseY)
     {
      //--- Compute how far the cursor has moved since drag began
      int deltaX = mouseX - m_dragStartX;
      int deltaY = mouseY - m_dragStartY;
      //--- Compute the candidate new canvas position
      int newX = m_canvasStartX + deltaX;
      int newY = m_canvasStartY + deltaY;
      //--- Retrieve chart pixel dimensions for clamping
      int chartWidth  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      //--- Constrain so the canvas cannot be dragged off screen
      newX = MathMax(0, MathMin(chartWidth  - m_currentWidth,  newX));
      newY = MathMax(0, MathMin(chartHeight - m_currentHeight, newY));
      //--- Store the accepted new position
      m_currentPositionX = newX;
      m_currentPositionY = newY;
      //--- Push the new position to the chart object
      ObjectSetInteger(0, m_canvasObjectName, OBJPROP_XDISTANCE, m_currentPositionX);
      ObjectSetInteger(0, m_canvasObjectName, OBJPROP_YDISTANCE, m_currentPositionY);
      ChartRedraw();
     }

   //+------------------------------------------------------------------+
   //| Resize the canvas and rebuild the projection on dimension change |
   //+------------------------------------------------------------------+
   void handleCanvasResize(int mouseX, int mouseY)
     {
      //--- Compute how far the cursor has moved since resize began
      int deltaX = mouseX - m_resizeStartX;
      int deltaY = mouseY - m_resizeStartY;
      //--- Start from current dimensions as candidate values
      int newWidth  = m_currentWidth;
      int newHeight = m_currentHeight;

      //--- Adjust width when resizing the right edge or corner
      if(m_activeResizeMode == RESIZE_RIGHT_EDGE || m_activeResizeMode == RESIZE_CORNER)
         newWidth = MathMax(MIN_CANVAS_WIDTH, m_resizeInitialWidth + deltaX);
      //--- Adjust height when resizing the bottom edge or corner
      if(m_activeResizeMode == RESIZE_BOTTOM_EDGE || m_activeResizeMode == RESIZE_CORNER)
         newHeight = MathMax(MIN_CANVAS_HEIGHT, m_resizeInitialHeight + deltaY);

      //--- Retrieve chart pixel dimensions for clamping
      int chartWidth  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      //--- Prevent the canvas from extending past chart boundaries
      newWidth  = MathMin(newWidth,  chartWidth  - m_currentPositionX - 10);
      newHeight = MathMin(newHeight, chartHeight - m_currentPositionY - 10);

      //--- Apply changes only when at least one dimension has changed
      if(newWidth != m_currentWidth || newHeight != m_currentHeight)
        {
         m_currentWidth  = newWidth;
         m_currentHeight = newHeight;
         //--- Resize the underlying canvas buffer
         m_mainCanvas.Resize(m_currentWidth, m_currentHeight);
         //--- Update the chart object size to match
         ObjectSetInteger(0, m_canvasObjectName, OBJPROP_XSIZE, m_currentWidth);
         ObjectSetInteger(0, m_canvasObjectName, OBJPROP_YSIZE, m_currentHeight);
         //--- Rebuild the projection matrix when in 3D mode
         if(m_currentViewMode == VIEW_3D_MODE)
           {
            //--- Inform DirectX of the new render target dimensions
            DXContextSetSize(m_mainCanvas.DXContext(), m_currentWidth, m_currentHeight);
            //--- Recalculate projection with the updated aspect ratio
            m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                            (float)m_currentWidth / (float)m_currentHeight,
                                            0.1f, 1000.0f);
            //--- Recompute the camera view transform
            updateCameraPosition();
           }
         //--- Re-render the scene at the new resolution
         renderVisualization();
         ChartRedraw();
        }
     }

   //+------------------------------------------------------------------+
   //| Return a lighter version of a colour by blending toward white    |
   //+------------------------------------------------------------------+
   color LightenColor(color baseColor, double factor)
     {
      uchar r = (uchar)((baseColor >> 16) & 0xFF);
      uchar g = (uchar)((baseColor >> 8)  & 0xFF);
      uchar b = (uchar)( baseColor         & 0xFF);
      r = (uchar)MathMin(255, r + (255 - r) * factor);
      g = (uchar)MathMin(255, g + (255 - g) * factor);
      b = (uchar)MathMin(255, b + (255 - b) * factor);
      return (r << 16) | (g << 8) | b;
     }

   //+------------------------------------------------------------------+
   //| Return a darker version of a colour by scaling toward black      |
   //+------------------------------------------------------------------+
   color DarkenColor(color baseColor, double factor)
     {
      uchar r = (uchar)((baseColor >> 16) & 0xFF);
      uchar g = (uchar)((baseColor >> 8)  & 0xFF);
      uchar b = (uchar)( baseColor         & 0xFF);
      r = (uchar)(r * (1.0 - factor));
      g = (uchar)(g * (1.0 - factor));
      b = (uchar)(b * (1.0 - factor));
      return (r << 16) | (g << 8) | b;
     }

   //+------------------------------------------------------------------+
   //| Linearly interpolate between two colours by a blend factor       |
   //+------------------------------------------------------------------+
   color InterpolateColors(color startColor, color endColor, double factor)
     {
      uchar r1 = (uchar)((startColor >> 16) & 0xFF);
      uchar g1 = (uchar)((startColor >> 8)  & 0xFF);
      uchar b1 = (uchar)( startColor         & 0xFF);
      uchar r2 = (uchar)((endColor >> 16) & 0xFF);
      uchar g2 = (uchar)((endColor >> 8)  & 0xFF);
      uchar b2 = (uchar)( endColor         & 0xFF);
      uchar r  = (uchar)(r1 + factor * (r2 - r1));
      uchar g  = (uchar)(g1 + factor * (g2 - g1));
      uchar b  = (uchar)(b1 + factor * (b2 - b1));
      return (r << 16) | (g << 8) | b;
     }

   //+------------------------------------------------------------------+
   //| Alpha-composite a source pixel over the existing canvas pixel    |
   //+------------------------------------------------------------------+
   void blendPixelSet(CCanvas &canvas, int x, int y, uint src)
     {
      //--- Reject pixels outside the canvas bounds
      if(x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
      //--- Read the current destination pixel
      uint dst = canvas.PixelGet(x, y);
      //--- Unpack source ARGB components to normalised floats
      double sa = ((src >> 24) & 0xFF) / 255.0;
      double sr = ((src >> 16) & 0xFF) / 255.0;
      double sg = ((src >> 8)  & 0xFF) / 255.0;
      double sb = ( src         & 0xFF) / 255.0;
      //--- Unpack destination ARGB components to normalised floats
      double da = ((dst >> 24) & 0xFF) / 255.0;
      double dr = ((dst >> 16) & 0xFF) / 255.0;
      double dg = ((dst >> 8)  & 0xFF) / 255.0;
      double db = ( dst         & 0xFF) / 255.0;
      //--- Compute the composited alpha (Porter-Duff "over")
      double outA = sa + da * (1 - sa);
      //--- Handle fully transparent result to avoid division by zero
      if(outA == 0) { canvas.PixelSet(x, y, 0); return; }
      //--- Compute composited RGB channels
      double outR = (sr * sa + dr * da * (1 - sa)) / outA;
      double outG = (sg * sa + dg * da * (1 - sa)) / outA;
      double outB = (sb * sa + db * da * (1 - sa)) / outA;
      //--- Convert back to byte components and pack
      uchar oa  = (uchar)(outA * 255 + 0.5);
      uchar or_ = (uchar)(outR * 255 + 0.5);
      uchar og  = (uchar)(outG * 255 + 0.5);
      uchar ob  = (uchar)(outB * 255 + 0.5);
      uint outCol = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob;
      canvas.PixelSet(x, y, outCol);
     }

   //+------------------------------------------------------------------+
   //| Fill the canvas body with a vertical gradient background         |
   //+------------------------------------------------------------------+
   void drawGradientBackground()
     {
      //--- Derive the bottom gradient colour by lightly tinting the theme
      color bottomColor = LightenColor(themeColor, 0.85);
      //--- Iterate every pixel row below the header bar
      for(int y = HEADER_BAR_HEIGHT; y < m_currentHeight; y++)
        {
         double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) /
                                 (double)(m_currentHeight - HEADER_BAR_HEIGHT);
         color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor);
         uchar alphaChannel    = (uchar)(255 * backgroundOpacityLevel);
         uint  argbColor       = ColorToARGB(currentRowColor, alphaChannel);
         //--- Paint every pixel in this row with the computed colour
         for(int x = 0; x < m_currentWidth; x++)
            m_mainCanvas.PixelSet(x, y, argbColor);
        }
     }

   //+------------------------------------------------------------------+
   //| Draw the outer and inner border rectangles around the canvas     |
   //+------------------------------------------------------------------+
   void drawCanvasBorder()
     {
      //--- Skip the border if the feature is disabled
      if(!showBorderFrame) return;
      //--- Use a slightly darker border when hovering a resize zone
      color borderColor = m_isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor;
      uint  argbBorder  = ColorToARGB(borderColor, 255);
      m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, m_currentHeight - 1, argbBorder);
      m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, m_currentHeight - 2, argbBorder);
     }

   //+------------------------------------------------------------------+
   //| Draw the outer and inner border rectangles for the 3D overlay    |
   //+------------------------------------------------------------------+
   void draw3DBorder()
     {
      color borderColor = m_isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor;
      uint  argbBorder  = ColorToARGB(borderColor, 255);
      m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, m_currentHeight - 1, argbBorder);
      m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, m_currentHeight - 2, argbBorder);
     }

   //+------------------------------------------------------------------+
   //| Draw the header bar with title text and the mode switch icon     |
   //+------------------------------------------------------------------+
   void drawHeaderBar()
     {
      //--- Compute the header fill colour from the current interaction state
      color headerColor;
      if(m_isDragging)
         headerColor = DarkenColor(themeColor, 0.1);       // Slightly darker while dragging
      else if(m_isHoveringHeader)
         headerColor = LightenColor(themeColor, 0.4);      // Medium light on hover
      else
         headerColor = LightenColor(themeColor, 0.7);      // Very light at rest

      uint argbHeader = ColorToARGB(headerColor, 255);
      m_mainCanvas.FillRectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbHeader);

      //--- Overlay the border frame on the header if enabled
      if(showBorderFrame)
        {
         uint argbBorder = ColorToARGB(themeColor, 255);
         m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbBorder);
         m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
        }

      //--- Draw the title text centred in the header
      m_mainCanvas.FontSet("Arial Bold", titleFontSize);
      uint argbText = ColorToARGB(titleTextColor, 255);
      string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f)", numTrials, successProbability);
      m_mainCanvas.TextOut(m_currentWidth / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2,
                           titleText, argbText, TA_CENTER);
      //--- Draw the interactive 2D/3D mode switch icon
      drawSwitchIcon();
     }

   //+------------------------------------------------------------------+
   //| Draw the circular 2D/3D toggle icon in the header bar           |
   //+------------------------------------------------------------------+
   void drawSwitchIcon()
     {
      //--- Compute the icon's top-left corner from the canvas right margin
      int iconX = m_currentWidth  - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
      //--- Vertically centre the icon within the header bar
      int iconY = (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;

      //--- Compute icon background colour from hover state
      color iconBgColor = m_isHoveringSwitchIcon
                          ? DarkenColor(themeColor, 0.1)
                          : LightenColor(themeColor, 0.5);
      uint argbIconBg = ColorToARGB(iconBgColor, 255);

      //--- Fill and border the circular icon
      m_mainCanvas.FillCircle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                              SWITCH_ICON_SIZE / 2, argbIconBg);
      uint argbBorder = ColorToARGB(themeColor, 255);
      m_mainCanvas.Circle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                          SWITCH_ICON_SIZE / 2, argbBorder);

      //--- Draw the "2D" or "3D" mode label inside the icon
      m_mainCanvas.FontSet("Arial Bold", 10);
      uint argbLabel = ColorToARGB(clrWhite, 255);
      string modeLabel = (m_currentViewMode == VIEW_2D_MODE) ? "2D" : "3D";
      m_mainCanvas.TextOut(iconX + SWITCH_ICON_SIZE / 2, iconY + (SWITCH_ICON_SIZE - 10) / 2,
                           modeLabel, argbLabel, TA_CENTER);
     }

   //+------------------------------------------------------------------+
   //| Draw the header bar overlaid on the 3D rendered scene           |
   //+------------------------------------------------------------------+
   void drawHeaderBarOn3D()
     {
      //--- Compute the header fill colour from the current interaction state
      color headerColor;
      if(m_isDragging)
         headerColor = DarkenColor(themeColor, 0.1);
      else if(m_isHoveringHeader)
         headerColor = LightenColor(themeColor, 0.4);
      else
         headerColor = LightenColor(themeColor, 0.7);

      uint argbHeader = ColorToARGB(headerColor, 255);
      m_mainCanvas.FillRectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbHeader);

      //--- Overlay the border frame on the header if enabled
      if(showBorderFrame)
        {
         uint argbBorder = ColorToARGB(themeColor, 255);
         m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbBorder);
         m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
        }

      //--- Draw the 3D title text centred in the header
      m_mainCanvas.FontSet("Arial Bold", titleFontSize);
      uint argbText = ColorToARGB(titleTextColor, 255);
      string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f) - 3D View",
                                      numTrials, successProbability);
      m_mainCanvas.TextOut(m_currentWidth / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2,
                           titleText, argbText, TA_CENTER);
      //--- Draw the interactive 2D/3D mode switch icon
      drawSwitchIcon();
     }

   //+------------------------------------------------------------------+
   //| Draw histogram bars, PMF curve, axes and axis labels in 2D      |
   //+------------------------------------------------------------------+
   void drawDistributionPlot()
     {
      //--- Abort if data has not been loaded yet
      if(!m_isDataLoaded) return;

      //--- Define the outer boundaries of the plot area
      int plotAreaLeft   = 60;
      int plotAreaRight  = m_currentWidth  - 40;
      int plotAreaTop    = HEADER_BAR_HEIGHT + 10;
      int plotAreaBottom = m_currentHeight  - 50;

      //--- Inset the drawable area by the configured padding
      int drawAreaLeft   = plotAreaLeft   + plotPadding;
      int drawAreaRight  = plotAreaRight  - plotPadding;
      int drawAreaTop    = plotAreaTop    + plotPadding;
      int drawAreaBottom = plotAreaBottom - plotPadding;

      //--- Compute drawable dimensions
      int plotWidth  = drawAreaRight - drawAreaLeft;
      int plotHeight = drawAreaBottom - drawAreaTop;
      if(plotWidth <= 0 || plotHeight <= 0) return;

      //--- Compute data ranges for axis mapping
      double rangeX = m_maxDataValue - m_minDataValue;
      double rangeY = m_maxTheoreticalValue;
      if(rangeX == 0) rangeX = 1;
      if(rangeY == 0) rangeY = 1;

      uint argbAxisColor = ColorToARGB(clrBlack, 255);

      //--- Draw a two-pixel-thick Y axis line
      for(int thick = 0; thick < 2; thick++)
         m_mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor);
      //--- Draw a two-pixel-thick X axis line
      for(int thick = 0; thick < 2; thick++)
         m_mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor);

      //--- Draw Y-axis tick marks and labels
      m_mainCanvas.FontSet("Arial", axisLabelFontSize);
      uint argbTickLabel = ColorToARGB(clrBlack, 255);
      int numYTicks = 5;
      for(int i = 0; i <= numYTicks; i++)
        {
         double yValue = (rangeY * i) / numYTicks;
         int    yPos   = drawAreaBottom - (int)(yValue / rangeY * plotHeight);
         m_mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor);
         string yLabel = DoubleToString(yValue, 3);
         m_mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize / 2,
                              yLabel, argbTickLabel, TA_RIGHT);
        }

      //--- Draw X-axis tick marks and labels
      int numXTicks = MathMin(10, histogramCells);
      for(int i = 0; i <= numXTicks; i++)
        {
         double xValue = m_minDataValue + (rangeX * i) / numXTicks;
         int    xPos   = drawAreaLeft + (int)((xValue - m_minDataValue) / rangeX * plotWidth);
         m_mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor);
         string xLabel = DoubleToString(xValue, 0);
         m_mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER);
        }

      //--- Draw histogram bars
      uint argbHist = ColorToARGB(histogramColor, 255);
      double totalGaps = (histogramCells - 1) * histogramGapPixels;
      double barWidth  = (plotWidth - totalGaps) / histogramCells;
      if(barWidth < 1) barWidth = 1;
      for(int i = 0; i < histogramCells; i++)
        {
         int barLeft   = drawAreaLeft + (int)(i * (barWidth + histogramGapPixels));
         int barRight  = barLeft + (int)barWidth - 1;
         int barHeight = (int)(m_histogramFrequencies[i] / rangeY * plotHeight);
         int barTop    = drawAreaBottom - barHeight;
         if(barRight >= barLeft)
            m_mainCanvas.FillRectangle(barLeft, barTop, barRight, drawAreaBottom, argbHist);
        }

      //--- Draw the theoretical PMF curve with anti-aliased line segments
      uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
      for(int i = 0; i < ArraySize(m_theoreticalXValues) - 1; i++)
        {
         int x1 = drawAreaLeft + (int)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * plotWidth);
         int y1 = drawAreaBottom - (int)(m_theoreticalYValues[i]     / rangeY * plotHeight);
         int x2 = drawAreaLeft + (int)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * plotWidth);
         int y2 = drawAreaBottom - (int)(m_theoreticalYValues[i + 1] / rangeY * plotHeight);
         for(int w = 0; w < curveLineWidth; w++)
            m_mainCanvas.LineAA(x1, y1 + w, x2, y2 + w, argbCurve);
        }

      //--- Draw the horizontal and vertical axis labels
      m_mainCanvas.FontSet("Arial Bold", labelFontSize);
      uint argbAxisLabel = ColorToARGB(clrBlack, 255);
      string xAxisLabel = "Number of Successes (k)";
      m_mainCanvas.TextOut(m_currentWidth / 2, m_currentHeight - 20, xAxisLabel, argbAxisLabel, TA_CENTER);
      string yAxisLabel = "Probability / Scaled Frequency";
      m_mainCanvas.FontAngleSet(900);
      m_mainCanvas.TextOut(12, m_currentHeight / 2, yAxisLabel, argbAxisLabel, TA_CENTER);
      m_mainCanvas.FontAngleSet(0);
     }

   //+------------------------------------------------------------------+
   //| Draw the statistics panel with descriptive stat labels           |
   //+------------------------------------------------------------------+
   void drawStatisticsPanel()
     {
      //--- Compute panel absolute position
      int panelX      = statsPanelX;
      int panelY      = HEADER_BAR_HEIGHT + statsPanelY;
      int panelWidth  = statsPanelWidth;
      int panelHeight = statsPanelHeight;

      //--- Compute panel background colour as a very light theme tint
      color panelBgColor = LightenColor(themeColor, 0.9);
      uchar bgAlpha      = 153;
      uint  argbPanelBg  = ColorToARGB(panelBgColor, bgAlpha);
      uint  argbBorder   = ColorToARGB(themeColor, 255);
      uint  argbText     = ColorToARGB(clrBlack, 255);

      //--- Flood-fill the panel background with alpha blending
      for(int y = panelY; y <= panelY + panelHeight; y++)
         for(int x = panelX; x <= panelX + panelWidth; x++)
            blendPixelSet(m_mainCanvas, x, y, argbPanelBg);

      //--- Draw panel border lines
      for(int x = panelX; x <= panelX + panelWidth; x++)
         blendPixelSet(m_mainCanvas, x, panelY, argbBorder); // Top
      for(int y = panelY; y <= panelY + panelHeight; y++)
         blendPixelSet(m_mainCanvas, panelX + panelWidth, y, argbBorder); // Right
      for(int y = panelY; y <= panelY + panelHeight; y++)
         blendPixelSet(m_mainCanvas, panelX, y, argbBorder); // Left

      //--- Draw all statistic text rows
      m_mainCanvas.FontSet("Arial", panelFontSize);
      int textY      = panelY + 6;
      int lineSpacing = panelFontSize + 1;

      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Trials (n): %d", numTrials), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Prob (p): %.2f", successProbability), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Sample: %d", sampleSize), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Mean: %.2f", m_sampleMean), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("StdDev: %.2f", m_sampleStandardDeviation), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Skewness: %.3f", m_sampleSkewness), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Kurtosis: %.3f", m_sampleKurtosis), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Q1 (25%%): %.1f", m_percentile25), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Median (50%%): %.1f", m_percentile50), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY, StringFormat("Q3 (75%%): %.1f", m_percentile75), argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY,
                           StringFormat("95%% CI: [%.2f, %.2f]",
                                        m_confidenceInterval95Lower, m_confidenceInterval95Upper),
                           argbText, TA_LEFT);
      textY += lineSpacing;
      m_mainCanvas.TextOut(panelX + 8, textY,
                           StringFormat("99%% CI: [%.2f, %.2f]",
                                        m_confidenceInterval99Lower, m_confidenceInterval99Upper),
                           argbText, TA_LEFT);
     }

   //+------------------------------------------------------------------+
   //| Draw the statistics panel overlay on the 3D render              |
   //+------------------------------------------------------------------+
   void drawStatisticsPanelOn3D()
     {
      //--- Reuse the same 2D statistics panel for the 3D overlay
      drawStatisticsPanel();
     }

   //+------------------------------------------------------------------+
   //| Draw the legend panel with colour-coded sample and curve keys    |
   //+------------------------------------------------------------------+
   void drawLegend()
     {
      //--- Compute legend panel absolute position
      int legendX          = statsPanelX;
      int legendY          = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight;
      int legendWidth      = statsPanelWidth;
      int legendHeightThis = legendHeight;

      //--- Compute legend background colour as a very light theme tint
      color legendBgColor = LightenColor(themeColor, 0.9);
      uchar bgAlpha       = 153;
      uint  argbLegendBg  = ColorToARGB(legendBgColor, bgAlpha);
      uint  argbBorder    = ColorToARGB(themeColor, 255);
      uint  argbText      = ColorToARGB(clrBlack, 255);

      //--- Flood-fill the legend background with alpha blending
      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         for(int x = legendX; x <= legendX + legendWidth; x++)
            blendPixelSet(m_mainCanvas, x, y, argbLegendBg);

      //--- Draw all four legend border lines
      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, legendY, argbBorder); // Top
      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         blendPixelSet(m_mainCanvas, legendX + legendWidth, y, argbBorder); // Right
      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, legendY + legendHeightThis, argbBorder); // Bottom
      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         blendPixelSet(m_mainCanvas, legendX, y, argbBorder); // Left

      m_mainCanvas.FontSet("Arial", panelFontSize);
      int itemY      = legendY + 10;
      int lineSpacing = panelFontSize;

      //--- Draw histogram colour swatch and label
      uint argbHist = ColorToARGB(histogramColor, 255);
      m_mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist);
      m_mainCanvas.TextOut(legendX + 27, itemY - 4, "Sample Histogram", argbText, TA_LEFT);
      itemY += lineSpacing;

      //--- Draw PMF curve colour swatch and label
      uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
      for(int i = 0; i < 15; i++)
        {
         blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY,     argbCurve);
         blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY + 1, argbCurve);
        }
      m_mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT);
     }

   //+------------------------------------------------------------------+
   //| Draw the legend panel overlay on the 3D render                  |
   //+------------------------------------------------------------------+
   void drawLegendOn3D()
     {
      //--- Compute legend panel absolute position (same layout as 2D)
      int legendX          = statsPanelX;
      int legendY          = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight;
      int legendWidth      = statsPanelWidth;
      int legendHeightThis = legendHeight;

      color legendBgColor = LightenColor(themeColor, 0.9);
      uchar bgAlpha       = 153;
      uint  argbLegendBg  = ColorToARGB(legendBgColor, bgAlpha);
      uint  argbBorder    = ColorToARGB(themeColor, 255);
      uint  argbText      = ColorToARGB(clrBlack, 255);

      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         for(int x = legendX; x <= legendX + legendWidth; x++)
            blendPixelSet(m_mainCanvas, x, y, argbLegendBg);

      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, legendY, argbBorder); // Top
      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         blendPixelSet(m_mainCanvas, legendX + legendWidth, y, argbBorder); // Right
      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, legendY + legendHeightThis, argbBorder); // Bottom
      for(int y = legendY; y <= legendY + legendHeightThis; y++)
         blendPixelSet(m_mainCanvas, legendX, y, argbBorder); // Left

      m_mainCanvas.FontSet("Arial", panelFontSize);
      int itemY      = legendY + 10;
      int lineSpacing = panelFontSize;

      //--- Draw 3D histogram colour swatch and label
      uint argbHist = ColorToARGB(histogramColor, 255);
      m_mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist);
      m_mainCanvas.TextOut(legendX + 27, itemY - 4, "3D Histogram", argbText, TA_LEFT);
      itemY += lineSpacing;

      //--- Draw PMF curve colour swatch and label
      uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
      for(int i = 0; i < 15; i++)
        {
         blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY,     argbCurve);
         blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY + 1, argbCurve);
        }
      m_mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT);
     }

   //+------------------------------------------------------------------+
   //| Draw resize grip indicators for the active or hovered zone      |
   //+------------------------------------------------------------------+
   void drawResizeIndicator()
     {
      uint argbIndicator = ColorToARGB(themeColor, 255);

      //--- Draw the corner grip as a filled square with diagonal hatch lines
      if(m_hoverResizeMode == RESIZE_CORNER || m_activeResizeMode == RESIZE_CORNER)
        {
         int cornerX = m_currentWidth  - resizeGripSize;
         int cornerY = m_currentHeight - resizeGripSize;
         m_mainCanvas.FillRectangle(cornerX, cornerY,
                                    m_currentWidth - 1, m_currentHeight - 1, argbIndicator);
         //--- Overlay three diagonal hatch lines as a resize affordance cue
         for(int i = 0; i < 3; i++)
           {
            int offset = i * 3;
            m_mainCanvas.Line(cornerX + offset, m_currentHeight - 1,
                              m_currentWidth  - 1, cornerY + offset, argbIndicator);
           }
        }

      //--- Draw the right-edge grip as a vertical bar
      if(m_hoverResizeMode == RESIZE_RIGHT_EDGE || m_activeResizeMode == RESIZE_RIGHT_EDGE)
        {
         int indicatorY = m_currentHeight / 2 - 15;
         m_mainCanvas.FillRectangle(m_currentWidth - 3, indicatorY,
                                    m_currentWidth - 1, indicatorY + 30, argbIndicator);
        }

      //--- Draw the bottom-edge grip as a horizontal bar
      if(m_hoverResizeMode == RESIZE_BOTTOM_EDGE || m_activeResizeMode == RESIZE_BOTTOM_EDGE)
        {
         int indicatorX = m_currentWidth / 2 - 15;
         m_mainCanvas.FillRectangle(indicatorX, m_currentHeight - 3,
                                    indicatorX + 30, m_currentHeight - 1, argbIndicator);
        }
     }

   //+------------------------------------------------------------------+
   //| Draw resize grip indicators overlaid on the 3D render           |
   //+------------------------------------------------------------------+
   void drawResizeIndicatorOn3D()
     {
      //--- Reuse the same 2D resize indicator for the 3D overlay
      drawResizeIndicator();
     }
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
DistributionVisualizer *distributionVisualizer = NULL; // Pointer to the active visualizer instance

//+------------------------------------------------------------------+
//| Initialise the EA, create the canvas and load distribution data  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Enable mouse movement events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   //--- Enable mouse wheel events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);

   //--- Allocate and construct the visualizer object
   distributionVisualizer = new DistributionVisualizer();
   if(distributionVisualizer == NULL)
     {
      Print("ERROR: Failed to create window object");
      return INIT_FAILED;
     }

   //--- Create the canvas bitmap label and initialise the 3D scene
   if(!distributionVisualizer.createCanvasAndObjects())
     {
      Print("ERROR: Failed to create canvas");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Generate the binomial sample and compute all statistics
   if(!distributionVisualizer.loadDistributionData())
     {
      Print("ERROR: Failed to load distribution data");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Render the initial frame
   distributionVisualizer.renderVisualization();
   ChartRedraw();

   Print("SUCCESS: Distribution window initialized");
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Release all resources when the EA is removed                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Stop the animation timer
   EventKillTimer();
   //--- Destroy the visualizer object if it still exists
   if(distributionVisualizer != NULL)
     {
      delete distributionVisualizer;
      distributionVisualizer = NULL;
     }
   //--- Redraw the chart to remove the canvas bitmap object
   ChartRedraw();
   Print("Distribution window deinitialized");
  }

//+------------------------------------------------------------------+
//| Reload distribution data on each new bar                         |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Track the last processed bar open time across calls
   static datetime lastBarTimestamp = 0;
   //--- Read the current bar open time on the configured timeframe
   datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0);

   //--- Reload and redraw only when a new bar has formed
   if(currentBarTimestamp > lastBarTimestamp)
     {
      if(distributionVisualizer != NULL)
        {
         //--- Regenerate the binomial sample and statistics
         if(distributionVisualizer.loadDistributionData())
           {
            //--- Redraw the updated visualization
            distributionVisualizer.renderVisualization();
            ChartRedraw();
           }
        }
      //--- Update the bar timestamp to prevent repeated processing
      lastBarTimestamp = currentBarTimestamp;
     }
  }

//+------------------------------------------------------------------+
//| Advance the camera snap animation on each timer tick             |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Delegate to the visualizer's per-tick animation updater
   if(distributionVisualizer != NULL)
      distributionVisualizer.tickAnimation();
  }

//+------------------------------------------------------------------+
//| Route chart mouse, wheel and key events to the visualizer        |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Abort if the visualizer has not been initialised
   if(distributionVisualizer == NULL) return;

   //--- Handle mouse move and button events
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX     = (int)lparam;       // Horizontal cursor position in pixels
      int mouseY     = (int)dparam;       // Vertical cursor position in pixels
      int mouseState = (int)sparam;       // Bitmask of pressed mouse buttons

      distributionVisualizer.handleMouseEvent(mouseX, mouseY, mouseState);
     }

   //--- Handle mouse wheel scroll events
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Unpack cursor X from the low 16 bits of lparam
      int mouseX = (int)(short) lparam;
      //--- Unpack cursor Y from the high 16 bits of lparam
      int mouseY = (int)(short)(lparam >> 16);
      distributionVisualizer.handleMouseWheel(mouseX, mouseY, dparam);
     }

   //--- Handle Escape key press to exit pan mode
   if(id == CHARTEVENT_KEYDOWN && lparam == 27)
      distributionVisualizer.exitPanMode();
  }
//+------------------------------------------------------------------+