Files
2025-02-07 19:13:59 +03:30

559 lines
30 KiB
Plaintext

//+------------------------------------------------------------------+
//| OLAPGUI.mqh |
//| Copyright (c) 2019-2020, Marketeer |
//| https://www.mql5.com/en/users/marketeer |
//| Online Analytical Processing of trading hypercubes |
//| https://www.mql5.com/en/articles/6602 |
//| https://www.mql5.com/en/articles/6603 |
//| https://www.mql5.com/en/articles/7656 |
//+------------------------------------------------------------------+
#property copyright "Copyright (c) 2019-2020, Marketeer"
#property link "https://www.mql5.com/en/users/marketeer"
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\ListView.mqh>
#include <Controls\ComboBox.mqh>
#include <Layouts/Box.mqh>
#include <Layouts/ComboBoxResizable.mqh>
#include <PairPlot/Plot.mqh>
#include <Layouts/MaximizableAppDialog.mqh>
#include <OLAP/OLAPCommon.mqh>
#define BUTTON_WIDTH (100)
#define BUTTON_HEIGHT (20)
#define EDIT_HEIGHT (20)
#define GROUP_WIDTH (150)
#define LIST_HEIGHT (179)
#define RADIO_HEIGHT (56)
#define CHECK_HEIGHT (93)
#define AXES_NUMBER 3
#define ALGO_NUMBER 4
#define ALGO_AGGREGATOR 0
#define ALGO_FIELD 1
#define ALGO_SORTING 2
#define ALGO_GRAPHTYPE 3
class OLAPDialogBase: public MaximizableAppDialog
{
protected:
string selectors[/*MAX_AXES_CHOICES*/];
string settings[/*MAX_ALGO_CHOICES*/][ALGO_NUMBER];
int defaults[ALGO_NUMBER];
CBox m_main;
CBox m_row_1;
ComboBoxResizable m_axis[AXES_NUMBER];
CButton m_button_ok;
CBox m_row_2;
ComboBoxResizable m_algo[ALGO_NUMBER]; // aggregator, field, graph type, sort by
CBox m_row_plot;
CPlot m_plot;
ENUM_CURVE_TYPE curveType;
bool processing;
// 3-d dimension handling
bool browsing;
int currentZ;
int maxZ;
string titlesZ[];
void validateZ();
int customFieldCount;
string customFields[];
public:
virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) override;
virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;
virtual int process() = 0;
virtual void setup() = 0;
virtual void setCustomFields(const DataAdapter &adapter)
{
string names[];
if(adapter.getCustomFields(names) > 0)
{
customFieldCount = ArrayCopy(customFields, names);
}
}
// data callbacks
void accept1D(const PairArray *data, const string title);
void accept2D(const double &x[], const double &y[], const string title);
void finalize();
void registerZ(const string &titles[]);
int getCurrentZ() const
{
return currentZ;
}
protected:
CWnd *CreateAxisComboBox(const int i);
CWnd *CreateAlgoComboBox(const int i);
CWnd *CreatePlot(const long chart, const string name, const int subwin);
CWnd *CreateButton(const long chart, const string name, const int subwin);
bool OnChangeComboBoxAxis(const int i);
bool OnChangeComboBoxAlgo(const int i);
void OnClickButton(void);
virtual CWnd *CreateMain(const long chart, const string name, const int subwin);
virtual CWnd *CreateAxesRow(const long chart, const string name, const int subwin);
virtual CWnd *CreateAlgoRow(const long chart, const string name, const int subwin);
virtual CWnd *CreatePlotRow(const long chart, const string name, const int subwin);
virtual void SelfAdjustment(const bool restore = false) override;
};
// since MQL5 does not support multiple inheritence we need this delegate object
class OLAPDisplay: public Display
{
private:
OLAPDialogBase *parent;
bool printout;
public:
OLAPDisplay(OLAPDialogBase *ptr, const bool printLog = true): parent(ptr), printout(printLog) {}
void setPrintLog(const bool printLog) {printout = printLog;}
virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
};
EVENT_MAP_BEGIN(OLAPDialogBase)
ON_INDEXED_EVENT(ON_CHANGE, m_axis, OnChangeComboBoxAxis)
ON_INDEXED_EVENT(ON_CHANGE, m_algo, OnChangeComboBoxAlgo)
ON_EVENT(ON_CLICK, m_button_ok, OnClickButton)
EVENT_MAP_END(MaximizableAppDialog)
bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
{
setup();
const int maxw = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
const int maxh = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
int _x1 = x1;
int _y1 = y1;
int _x2 = x2;
int _y2 = y2;
if(x2 - x1 > maxw || x2 > maxw)
{
_x1 = 0;
_x2 = _x1 + maxw - 0;
}
if(y2 - y1 > maxh || y2 > maxh)
{
_y1 = 0;
_y2 = _y1 + maxh - 1;
}
if(MaximizableAppDialog::Create(chart, name, subwin, _x1, _y1, _x2, _y2)
&& Add(CreateMain(chart, name, subwin))
&& m_main.Pack())
{
SelfAdjustment();
m_axis[1].Disable();
m_axis[2].Disable();
return true;
}
return false;
}
CWnd *OLAPDialogBase::CreateMain(const long chart, const string name, const int subwin)
{
m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
if(m_main.Create(chart, name + "main", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight())
&& m_main.Add(CreateAxesRow(chart, name, subwin))
&& m_main.Add(CreateAlgoRow(chart, name, subwin))
&& m_main.Add(CreatePlotRow(chart, name, subwin)))
{
return &m_main;
}
return NULL;
}
CWnd *OLAPDialogBase::CreateAxesRow(const long chart, const string name, const int subwin)
{
if(m_row_1.Create(chart, name + "axesrow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5))
{
m_row_1.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
for(int i = 0; i < AXES_NUMBER; i++)
{
if(!m_row_1.Add(CreateAxisComboBox(i))) return NULL;
}
if(!m_row_1.Add(CreateButton(chart, name, subwin))) return NULL;
return &m_row_1;
}
return NULL;
}
CWnd *OLAPDialogBase::CreateAxisComboBox(const int x)
{
if(m_axis[x].Create(m_chart_id, m_name + "axis" + (string)x, m_subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT))
{
for(int i = 0; i < ArraySize(selectors); i++)
{
string prefix = i == 0 ? CharToString((uchar)('X' + x)) + ": " : "";
if(!m_axis[x].ItemAdd(prefix + selectors[i]))
return NULL;
}
m_axis[x].Select(0);
return &m_axis[x];
}
return NULL;
}
CWnd *OLAPDialogBase::CreateButton(const long chart, const string name, const int subwin)
{
if(m_button_ok.Create(chart, name + "ok", subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT)
&& m_button_ok.Text("Process"))
{
return &m_button_ok;
}
return NULL;
}
CWnd *OLAPDialogBase::CreateAlgoRow(const long chart, const string name, const int subwin)
{
if(m_row_2.Create(chart, name + "algorow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5))
{
m_row_2.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
for(int i = 0; i < ALGO_NUMBER; i++)
{
if(!m_row_2.Add(CreateAlgoComboBox(i))) return NULL;
}
return &m_row_2;
}
return NULL;
}
CWnd *OLAPDialogBase::CreateAlgoComboBox(const int x)
{
if(m_algo[x].Create(m_chart_id, m_name + "algo" + (string)x, m_subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT))
{
for(int i = 0; i < ArrayRange(settings, 0); i++)
{
if(settings[i][x] != NULL)
{
if(!m_algo[x].ItemAdd(settings[i][x])) return NULL;
}
}
m_algo[x].Select(defaults[x]);
return &m_algo[x];
}
return NULL;
}
CWnd *OLAPDialogBase::CreatePlotRow(const long chart, const string name, const int subwin)
{
m_row_plot.PaddingLeft(0);
m_row_plot.PaddingRight(0);
const int h = m_row_1.Height() + m_row_2.Height() + CONTROLS_BORDER_WIDTH * 4;
m_row_plot.Alignment(WND_ALIGN_CLIENT, CONTROLS_BORDER_WIDTH * 2, h, CONTROLS_BORDER_WIDTH * 2, CONTROLS_BORDER_WIDTH * 2);
if(m_row_plot.Create(chart, name + "plotrow", subwin, 0, 0, ClientAreaWidth(), LIST_HEIGHT)
&& m_row_plot.Add(CreatePlot(chart, name, subwin)))
{
return &m_row_plot;
}
return NULL;
}
CWnd *OLAPDialogBase::CreatePlot(const long chart, const string name, const int subwin)
{
if(m_plot.Create(chart, name + "Plot", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight(), curveType))
{
return &m_plot;
}
return NULL;
}
bool OLAPDialogBase::OnChangeComboBoxAxis(const int x)
{
if(browsing)
{
maxZ = 0;
validateZ();
}
int selection = (int)m_axis[x].Value();
for(int i = x + 1; i < AXES_NUMBER; i++)
{
if(selection > 0)
{
m_axis[i].Enable();
}
else
{
m_axis[i].Disable();
}
selection = (int)MathMin(selection, m_axis[i].Value());
}
/* Sorting is now enabled for ever, but sorting by _values_ for multidimensional data
is not coherent (each curve is sorted on its own, so X marks mean nothing)
int count = 0;
for(int i = 0; i < AXES_NUMBER; i++)
{
count += ((int)m_axis[i].Value() > 0 ? 1 : 0) * (i + 1);
}
if(count == 1) m_algo[2].Enable(); // sorting
else m_algo[2].Disable();
*/
return true;
}
bool OLAPDialogBase::OnChangeComboBoxAlgo(const int i)
{
if(browsing && i < 3)
{
maxZ = 0;
validateZ();
}
if(i == ALGO_GRAPHTYPE)
{
curveType = (ENUM_CURVE_TYPE)(m_algo[i].Value());
m_plot.SetDefaultCurveType(curveType);
m_plot.Refresh(true);
}
else
if(i == ALGO_AGGREGATOR)
{
AGGREGATORS a = (AGGREGATORS)(m_algo[i].Value());
if(a == AGGREGATOR_IDENTITY || a == AGGREGATOR_COUNT)
{
m_algo[1].Disable();
}
else
{
m_algo[1].Enable();
}
}
return true;
}
void OLAPDialogBase::OnClickButton(void)
{
if(processing) return;
if(browsing)
{
currentZ = (currentZ + 1) % maxZ;
validateZ();
}
processing = true;
const int n = process();
if(n == 0 && processing)
{
finalize();
}
}
void OLAPDialogBase::SelfAdjustment(const bool restore = false)
{
CSize min = m_main.GetMinSize();
CSize size;
size.cx = ClientAreaWidth();
size.cy = ClientAreaHeight();
if(restore)
{
if(min.cx > size.cx) size.cx = min.cx;
if(min.cy > size.cy) size.cy = min.cy;
}
m_main.Size(size);
int w = (m_row_1.Width() - 2 * 2 * 2 * 3) / 4;
for(int i = 0; i < AXES_NUMBER; i++)
{
m_axis[i].Width(w);
}
m_button_ok.Width(w);
for(int i = 0; i < ALGO_NUMBER; i++)
{
m_algo[i].Width(w);
}
if(!restore)
{
m_plot.Size(size.cx - CONTROLS_BORDER_WIDTH * 16, size.cy - m_row_1.Height() - m_row_2.Height() - CONTROLS_BORDER_WIDTH * 16);
m_plot.Resize(0, 0, size.cx - CONTROLS_BORDER_WIDTH * 16, size.cy - m_row_1.Height() - m_row_2.Height() - CONTROLS_BORDER_WIDTH * 16);
}
m_main.Pack();
m_plot.Refresh();
}
void OLAPDisplay::display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
{
int consts[];
const int selectorCount = metaData.getDimension();
ArrayResize(consts, selectorCount);
ArrayInitialize(consts, 0);
if(selectorCount == 1)
{
PairArray *result;
if(metaData.getVector(0, consts, result, sortby))
{
string title = metaData.getDimensionTitle(0);
if(printout)
{
Print("===== " + title + " =====");
ArrayPrint(result.array);
}
parent.accept1D(result, title);
}
parent.finalize();
return;
}
int dimensions[];
ArrayResize(dimensions, selectorCount);
for(int i = 0; i < selectorCount; i++)
{
dimensions[i] = metaData.getDimensionRange(i);
}
if(selectorCount == 3)
{
PairArray *result;
consts[0] = 0;
consts[1] = 0;
string s[];
if(metaData.getVector(2, consts, result, SORT_BY_LABEL_ASCENDING))
{
result.convert(s);
parent.registerZ(s);
delete result;
}
ArrayInitialize(consts, 0);
}
if(selectorCount >= 2)
{
int z = parent.getCurrentZ();
if((dimensions[1] == 2) && identity)
{
double x[];
double y[];
PairArray *result;
consts[1] = 0;
if(selectorCount == 3) consts[2] = z; // this has no effect yet because of identity
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE
{
if(printout)
{
Print("===== " + metaData.getDimensionTitle(-(1)) + " =====");
ArrayPrint(result.array);
}
result.convert(x);
delete result;
}
consts[1] = 1;
if(selectorCount == 3) consts[2] = z; // this has no effect yet because of identity
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE
{
if(printout)
{
Print("===== " + metaData.getDimensionTitle(-(2)) + " =====");
ArrayPrint(result.array);
}
result.convert(y);
delete result;
}
parent.accept2D(x, y, metaData.getDimensionTitle(-(1)) + " vs " + metaData.getDimensionTitle(-(2)));
}
else
{
// NB: identity aggregator is always 2D cube: [record number][selected fields number]
// we need special handling for it
if(selectorCount == 3)
{
consts[2] = z;
}
for(int j = 0; j < dimensions[1]; j++)
{
consts[1] = j;
PairArray *result;
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE - only labels sorting is valid!
{
string title = NULL;
if(identity)
{
title = metaData.getDimensionTitle(-(j + 1));
}
if(printout)
{
Print("===== " + (identity ? title : metaData.getDimensionIndexLabel(1, j)) + " =====");
ArrayPrint(result.array);
}
parent.accept1D(result, (identity ? title : metaData.getDimensionIndexLabel(1, j)));
}
}
}
parent.finalize();
}
}
void OLAPDialogBase::registerZ(const string &titles[])
{
if(!browsing)
{
maxZ = ArraySize(titles);
ArrayResize(titlesZ, maxZ); // shrink if necessary
ArrayCopy(titlesZ, titles);
currentZ = 0;
}
}
void OLAPDialogBase::validateZ()
{
if(maxZ > 0)
{
string switcher = StringFormat("%d/%d %s >>", currentZ + 1, maxZ, titlesZ[currentZ]);
Print("Current Z: ", switcher);
m_button_ok.Text(switcher);
browsing = true;
}
else
{
m_button_ok.Text("Process");
browsing = false;
}
}
void OLAPDialogBase::accept1D(const PairArray *data, const string title)
{
m_plot.CurveAdd(data, title);
}
void OLAPDialogBase::accept2D(const double &x[], const double &y[], const string title)
{
m_plot.CurveAdd(x, y, title);
}
void OLAPDialogBase::finalize()
{
m_plot.Refresh();
processing = false;
m_button_ok.Text("Process");
validateZ();
}