//+------------------------------------------------------------------+
//|                                        DX_3D_Bars_TickVolume.mq5 |
//+------------------------------------------------------------------+
#property copyright "MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.11"
#property strict

//+------------------------------------------------------------------+
//| DirectX canvas                                                   |
//+------------------------------------------------------------------+
#include <Canvas\Canvas3D.mqh>
#include <Canvas\DX\DXMath.mqh>
#include <Canvas\DX\DXMesh.mqh>

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input int InpBarsToShow=80;          // Number of market bars in the 3D view

//+------------------------------------------------------------------+
//| Global objects                                                   |
//+------------------------------------------------------------------+
CCanvas3D         canvas;
CDXMesh           mesh;

//+------------------------------------------------------------------+
//| Scene constants                                                  |
//+------------------------------------------------------------------+
const int         CANVAS_WIDTH=900;
const int         CANVAS_HEIGHT=560;

const float       ORIGIN_X=-3.2f;
const float       ORIGIN_Y=-3.0f;
const float       ORIGIN_Z=-6.2f;
const float       VOLUME_AXIS_LENGTH=6.4f;
const float       PRICE_AXIS_LENGTH=6.0f;
const float       TIME_AXIS_LENGTH=12.4f;
const float       AXIS_THICKNESS=0.055f;

//+------------------------------------------------------------------+
//| Camera and state                                                 |
//+------------------------------------------------------------------+
float             cam_angle_x=0.55f;
float             cam_angle_y=0.70f;
float             cam_dist=19.0f;
int               prev_mouse_x=-1,prev_mouse_y=-1;
bool              need_rebuild=false;
bool              mesh_initialized=false;

string            btn_name="btn_refresh_3d_bars";
string            lbl_title="lbl_3d_bars_title";
string            lbl_x="lbl_3d_bars_x";
string            lbl_y="lbl_3d_bars_y";
string            lbl_z="lbl_3d_bars_z";
string            lbl_time_hint="lbl_3d_bars_time_hint";
string            lbl_time_0="lbl_3d_bars_time_0";
string            lbl_time_1="lbl_3d_bars_time_1";
string            lbl_time_2="lbl_3d_bars_time_2";
string            lbl_time_3="lbl_3d_bars_time_3";
string            lbl_time_4="lbl_3d_bars_time_4";

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,1);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,1);
   EventSetMillisecondTimer(250);

   if(!canvas.CreateBitmapLabel("DX_3D_Bars",20,20,CANVAS_WIDTH,CANVAS_HEIGHT,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("Canvas create error: ",GetLastError());
      return(INIT_FAILED);
     }

   canvas.ProjectionMatrixSet((float)DX_PI/6,(float)CANVAS_WIDTH/(float)CANVAS_HEIGHT,0.1f,80.0f);
   canvas.LightColorSet(DXColor(1.0f,0.96f,0.86f,1.0f));
   canvas.LightDirectionSet(DXVector3(0.45f,-0.85f,0.35f));
   canvas.AmbientColorSet(DXColor(0.46f,0.48f,0.52f,1.0f));

   CreateUi();
   BuildMarketBars();
   UpdateCamera();
   RedrawScene();

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,0);
   ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,0);
   EventKillTimer();

   ObjectDelete(0,btn_name);
   ObjectDelete(0,lbl_title);
   ObjectDelete(0,lbl_x);
   ObjectDelete(0,lbl_y);
   ObjectDelete(0,lbl_z);
   ObjectDelete(0,lbl_time_hint);
   ObjectDelete(0,lbl_time_0);
   ObjectDelete(0,lbl_time_1);
   ObjectDelete(0,lbl_time_2);
   ObjectDelete(0,lbl_time_3);
   ObjectDelete(0,lbl_time_4);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(need_rebuild)
     {
      need_rebuild=false;
      BuildMarketBars();
      RedrawScene();
     }
  }

//+------------------------------------------------------------------+
//| Chart events                                                     |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      OnMouseMove((int)lparam,(int)dparam,(uint)sparam);
      return;
     }

   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      OnMouseWheel(dparam);
      return;
     }

   if(id==CHARTEVENT_OBJECT_CLICK && sparam==btn_name)
     {
      ObjectSetInteger(0,btn_name,OBJPROP_STATE,false);
      need_rebuild=true;
      ChartRedraw();
      return;
     }
  }

//+------------------------------------------------------------------+
//| UI                                                               |
//+------------------------------------------------------------------+
void CreateUi()
  {
   CreateLabel(lbl_title,20,4,"3D bars: price Y / tick volume X / time Z",clrBlack,10);
   CreateLabel(lbl_x,32,594,"X: Tick Volume",clrTomato,9);
   CreateLabel(lbl_y,180,594,"Y: Price High-Low",clrSeaGreen,9);
   CreateLabel(lbl_z,350,594,"Z: Time axis, old -> new",clrRoyalBlue,9);
   CreateLabel(lbl_time_hint,32,512,"DATES ON Z TIME AXIS, OLD -> NEW:",clrBlack,8);
   CreateLabel(lbl_time_0,32,532,"START",clrRoyalBlue,8);
   CreateLabel(lbl_time_1,205,532,"25%",clrRoyalBlue,8);
   CreateLabel(lbl_time_2,380,532,"50%",clrRoyalBlue,8);
   CreateLabel(lbl_time_3,555,532,"75%",clrRoyalBlue,8);
   CreateLabel(lbl_time_4,730,532,"NOW",clrRoyalBlue,8);

   ObjectCreate(0,btn_name,OBJ_BUTTON,0,0,0);
   ObjectSetInteger(0,btn_name,OBJPROP_XDISTANCE,720);
   ObjectSetInteger(0,btn_name,OBJPROP_YDISTANCE,588);
   ObjectSetInteger(0,btn_name,OBJPROP_XSIZE,200);
   ObjectSetInteger(0,btn_name,OBJPROP_YSIZE,28);
   ObjectSetInteger(0,btn_name,OBJPROP_COLOR,clrWhite);
   ObjectSetInteger(0,btn_name,OBJPROP_BGCOLOR,clrDarkSlateGray);
   ObjectSetInteger(0,btn_name,OBJPROP_BORDER_COLOR,clrSilver);
   ObjectSetInteger(0,btn_name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
   ObjectSetInteger(0,btn_name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0,btn_name,OBJPROP_SELECTABLE,false);
   ObjectSetInteger(0,btn_name,OBJPROP_HIDDEN,true);
   ObjectSetInteger(0,btn_name,OBJPROP_ZORDER,0);
   ObjectSetInteger(0,btn_name,OBJPROP_FONTSIZE,10);
   ObjectSetString(0,btn_name,OBJPROP_TEXT,"Update 3D bars");

   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Create chart label                                               |
//+------------------------------------------------------------------+
void CreateLabel(const string name,const int x,const int y,const string text,const color clr,const int font_size)
  {
   ObjectCreate(0,name,OBJ_LABEL,0,0,0);
   ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
   ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
   ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
   ObjectSetInteger(0,name,OBJPROP_BACK,false);
   ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
   ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
   ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
   ObjectSetString(0,name,OBJPROP_TEXT,text);
  }

//+------------------------------------------------------------------+
//| Build 3D bars from chart history                                 |
//+------------------------------------------------------------------+
void BuildMarketBars()
  {
   MqlRates rates[];
   int bars_to_copy=MathMax(10,MathMin(InpBarsToShow,250));
   int copied=CopyRates(_Symbol,_Period,0,bars_to_copy,rates);

   if(copied<=0)
     {
      Print("CopyRates error: ",GetLastError());
      return;
     }

   double min_price=DBL_MAX;
   double max_price=-DBL_MAX;
   long max_volume=1;

   for(int i=0;i<copied;i++)
     {
      min_price=MathMin(min_price,rates[i].low);
      max_price=MathMax(max_price,rates[i].high);
      max_volume=(long)MathMax((double)max_volume,(double)rates[i].tick_volume);
     }

   double price_range=max_price-min_price;
   if(price_range<=0.0)
      price_range=_Point;
   bool time_ascending=(rates[0].time<=rates[copied-1].time);

   UpdateTimeLabels(rates,copied,time_ascending);

   DXVertex vertices[];
   uint     indices[];
   int      vertex_count=0;
   int      index_count=0;

   AddAxes(vertices,indices,vertex_count,index_count);

   float slot=TIME_AXIS_LENGTH/(float)copied;
   float depth=MathMax(0.045f,slot*0.70f);

   for(int i=0;i<copied;i++)
     {
      int source_index=(time_ascending ? i : copied-1-i);
      MqlRates rate=rates[source_index];

      float z0=ORIGIN_Z+(float)i*slot+slot*0.15f;
      float z1=z0+depth;
      float y0=ORIGIN_Y+(float)((rate.low-min_price)/price_range)*PRICE_AXIS_LENGTH;
      float y1=ORIGIN_Y+(float)((rate.high-min_price)/price_range)*PRICE_AXIS_LENGTH;
      if(y1-y0<0.035f)
         y1=y0+0.035f;

      float x1=ORIGIN_X+(float)((double)rate.tick_volume/(double)max_volume)*VOLUME_AXIS_LENGTH;
      if(x1-ORIGIN_X<0.06f)
         x1=ORIGIN_X+0.06f;

      DXColor bar_color;
      if(rate.close>=rate.open)
         bar_color=DXColor(0.08f,0.66f,0.36f,1.0f);
      else
         bar_color=DXColor(0.86f,0.18f,0.16f,1.0f);

      AddBox(vertices,indices,vertex_count,index_count,
             ORIGIN_X,x1,y0,y1,z0,z1,bar_color);
     }

   if(mesh_initialized)
      mesh.Shutdown();

   if(!mesh.Create(canvas.DXDispatcher(),canvas.InputScene(),vertices,indices))
     {
      Print("Mesh create error: ",GetLastError());
      return;
     }

   mesh.DiffuseColorSet(DXColor(1.0f,1.0f,1.0f,1.0f));

   if(!mesh_initialized)
     {
      canvas.ObjectAdd(&mesh);
      mesh_initialized=true;
     }
  }

//+------------------------------------------------------------------+
//| Update readable Z axis time labels                               |
//+------------------------------------------------------------------+
void UpdateTimeLabels(const MqlRates &rates[],const int copied,const bool time_ascending)
  {
   if(copied<=0)
      return;

   UpdateOneTimeLabel(lbl_time_0,rates,copied,time_ascending,0.00,"START ");
   UpdateOneTimeLabel(lbl_time_1,rates,copied,time_ascending,0.25,"25% ");
   UpdateOneTimeLabel(lbl_time_2,rates,copied,time_ascending,0.50,"50% ");
   UpdateOneTimeLabel(lbl_time_3,rates,copied,time_ascending,0.75,"75% ");
   UpdateOneTimeLabel(lbl_time_4,rates,copied,time_ascending,1.00,"NOW ");
  }

//+------------------------------------------------------------------+
//| Update one Z axis time label                                     |
//+------------------------------------------------------------------+
void UpdateOneTimeLabel(const string name,const MqlRates &rates[],const int copied,
                        const bool time_ascending,const double part,const string prefix)
  {
   int bar_index=(int)MathRound((copied-1)*part);
   bar_index=MathMax(0,MathMin(copied-1,bar_index));

   int source_index=(time_ascending ? bar_index : copied-1-bar_index);
   string text=prefix+TimeToString(rates[source_index].time,TIME_DATE)+" "+
               TimeToString(rates[source_index].time,TIME_MINUTES);
   ObjectSetString(0,name,OBJPROP_TEXT,text);
  }

//+------------------------------------------------------------------+
//| Add colored axes as thin boxes                                   |
//+------------------------------------------------------------------+
void AddAxes(DXVertex &vertices[],uint &indices[],int &vertex_count,int &index_count)
  {
   AddBox(vertices,indices,vertex_count,index_count,
          ORIGIN_X,ORIGIN_X+VOLUME_AXIS_LENGTH,
          ORIGIN_Y-AXIS_THICKNESS,ORIGIN_Y+AXIS_THICKNESS,
          ORIGIN_Z-AXIS_THICKNESS,ORIGIN_Z+AXIS_THICKNESS,
          DXColor(0.90f,0.18f,0.12f,1.0f));

   AddBox(vertices,indices,vertex_count,index_count,
          ORIGIN_X-AXIS_THICKNESS,ORIGIN_X+AXIS_THICKNESS,
          ORIGIN_Y,ORIGIN_Y+PRICE_AXIS_LENGTH,
          ORIGIN_Z-AXIS_THICKNESS,ORIGIN_Z+AXIS_THICKNESS,
          DXColor(0.08f,0.58f,0.26f,1.0f));

   AddBox(vertices,indices,vertex_count,index_count,
          ORIGIN_X-AXIS_THICKNESS,ORIGIN_X+AXIS_THICKNESS,
          ORIGIN_Y-AXIS_THICKNESS,ORIGIN_Y+AXIS_THICKNESS,
          ORIGIN_Z,ORIGIN_Z+TIME_AXIS_LENGTH,
          DXColor(0.10f,0.28f,0.86f,1.0f));

   for(int i=0;i<5;i++)
     {
      float z=ORIGIN_Z+TIME_AXIS_LENGTH*(float)i/4.0f;
      AddBox(vertices,indices,vertex_count,index_count,
             ORIGIN_X-0.18f,ORIGIN_X+0.18f,
             ORIGIN_Y-0.18f,ORIGIN_Y+0.18f,
             z-AXIS_THICKNESS,z+AXIS_THICKNESS,
             DXColor(0.05f,0.16f,0.72f,1.0f));
     }
  }

//+------------------------------------------------------------------+
//| Add a rectangular prism                                          |
//+------------------------------------------------------------------+
void AddBox(DXVertex &vertices[],uint &indices[],int &vertex_count,int &index_count,
            const float x0,const float x1,const float y0,const float y1,const float z0,const float z1,
            const DXColor &clr)
  {
   float xa=MathMin(x0,x1);
   float xb=MathMax(x0,x1);
   float ya=MathMin(y0,y1);
   float yb=MathMax(y0,y1);
   float za=MathMin(z0,z1);
   float zb=MathMax(z0,z1);

   AddFace(vertices,indices,vertex_count,index_count,xa,ya,zb,xb,ya,zb,xb,yb,zb,xa,yb,zb,0.0f,0.0f,1.0f,clr);
   AddFace(vertices,indices,vertex_count,index_count,xb,ya,za,xa,ya,za,xa,yb,za,xb,yb,za,0.0f,0.0f,-1.0f,clr);
   AddFace(vertices,indices,vertex_count,index_count,xb,ya,zb,xb,ya,za,xb,yb,za,xb,yb,zb,1.0f,0.0f,0.0f,clr);
   AddFace(vertices,indices,vertex_count,index_count,xa,ya,za,xa,ya,zb,xa,yb,zb,xa,yb,za,-1.0f,0.0f,0.0f,clr);
   AddFace(vertices,indices,vertex_count,index_count,xa,yb,zb,xb,yb,zb,xb,yb,za,xa,yb,za,0.0f,1.0f,0.0f,clr);
   AddFace(vertices,indices,vertex_count,index_count,xa,ya,za,xb,ya,za,xb,ya,zb,xa,ya,zb,0.0f,-1.0f,0.0f,clr);
  }

//+------------------------------------------------------------------+
//| Add one quad face                                                |
//+------------------------------------------------------------------+
void AddFace(DXVertex &vertices[],uint &indices[],int &vertex_count,int &index_count,
             const float x0,const float y0,const float z0,
             const float x1,const float y1,const float z1,
             const float x2,const float y2,const float z2,
             const float x3,const float y3,const float z3,
             const float nx,const float ny,const float nz,const DXColor &clr)
  {
   int base=vertex_count;
   ArrayResize(vertices,vertex_count+4);
   ArrayResize(indices,index_count+6);

   SetVertex(vertices[base],x0,y0,z0,nx,ny,nz,clr,0.0f,0.0f);
   SetVertex(vertices[base+1],x1,y1,z1,nx,ny,nz,clr,1.0f,0.0f);
   SetVertex(vertices[base+2],x2,y2,z2,nx,ny,nz,clr,1.0f,1.0f);
   SetVertex(vertices[base+3],x3,y3,z3,nx,ny,nz,clr,0.0f,1.0f);

   indices[index_count++]=(uint)base;
   indices[index_count++]=(uint)(base+1);
   indices[index_count++]=(uint)(base+2);
   indices[index_count++]=(uint)base;
   indices[index_count++]=(uint)(base+2);
   indices[index_count++]=(uint)(base+3);

   vertex_count+=4;
  }

//+------------------------------------------------------------------+
//| Fill vertex                                                      |
//+------------------------------------------------------------------+
void SetVertex(DXVertex &vertex,const float x,const float y,const float z,
               const float nx,const float ny,const float nz,const DXColor &clr,
               const float u,const float v)
  {
   vertex.position=DXVector4(x,y,z,1.0f);
   vertex.vcolor=clr;
   vertex.normal=DXVector4(nx,ny,nz,0.0f);
   vertex.tcoord=DXVector2(u,v);
  }

//+------------------------------------------------------------------+
//| Camera                                                           |
//+------------------------------------------------------------------+
void UpdateCamera()
  {
   float target_x=ORIGIN_X+VOLUME_AXIS_LENGTH*0.45f;
   float target_y=ORIGIN_Y+PRICE_AXIS_LENGTH*0.45f;
   float target_z=ORIGIN_Z+TIME_AXIS_LENGTH*0.50f;

   float x=target_x+(float)(cam_dist*MathSin(cam_angle_y)*MathCos(cam_angle_x));
   float y=target_y+(float)(cam_dist*MathSin(cam_angle_x));
   float z=target_z+(float)(cam_dist*MathCos(cam_angle_y)*MathCos(cam_angle_x));

   canvas.ViewPositionSet(DXVector3(x,y,z));
   canvas.ViewTargetSet(DXVector3(target_x,target_y,target_z));
   canvas.ViewUpDirectionSet(DXVector3(0.0f,1.0f,0.0f));
  }

//+------------------------------------------------------------------+
//| Mouse move                                                       |
//+------------------------------------------------------------------+
void OnMouseMove(int x,int y,uint flags)
  {
   if(prev_mouse_x==-1)
     {
      prev_mouse_x=x;
      prev_mouse_y=y;
      return;
     }

   if((flags&1)==1)
     {
      cam_angle_y+=(x-prev_mouse_x)/300.0f;
      cam_angle_x+=(y-prev_mouse_y)/300.0f;

      if(cam_angle_x<-DX_PI_DIV2*0.88f)
         cam_angle_x=-DX_PI_DIV2*0.88f;
      if(cam_angle_x>DX_PI_DIV2*0.88f)
         cam_angle_x=DX_PI_DIV2*0.88f;

      UpdateCamera();
      RedrawScene();
     }

   prev_mouse_x=x;
   prev_mouse_y=y;
  }

//+------------------------------------------------------------------+
//| Mouse wheel                                                      |
//+------------------------------------------------------------------+
void OnMouseWheel(double delta)
  {
   cam_dist*=1.0f-(float)delta*0.001f;
   cam_dist=MathMax(7.0f,MathMin(45.0f,cam_dist));
   UpdateCamera();
   RedrawScene();
  }

//+------------------------------------------------------------------+
//| Render                                                           |
//+------------------------------------------------------------------+
void RedrawScene()
  {
   canvas.Render(DX_CLEAR_COLOR|DX_CLEAR_DEPTH,ColorToARGB(clrWhiteSmoke));
   canvas.Update();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
