#ifndef HISTOGRAM_2D_H #define HISTOGRAM_2D_H #include "qcustomplot.h" #include "macro.h" const QList> colorCycle = { {QColor(Qt::red), "Red"}, {QColor(Qt::blue), "Blue"}, {QColor(Qt::darkGreen), "Dark Geen"}, {QColor(Qt::darkCyan), "Dark Cyan"}, {QColor(Qt::darkYellow), "Drak Yellow"}, {QColor(Qt::magenta), "Magenta"}, {QColor(Qt::darkMagenta), "Dark Magenta"}, {QColor(Qt::gray), "Gray"}}; //^============================================== //^============================================== class Histogram2D : public QCustomPlot{ public: Histogram2D(QString title, QString xLabel, QString yLabel, int xbin, double xmin, double xmax, int ybin, double ymin, double ymax, QWidget * parent = nullptr, QString defaultPath = QDir::homePath()); void SetXTitle(QString xTitle) { xAxis->setLabel(xTitle);} void SetYTitle(QString yTitle) { yAxis->setLabel(yTitle);} void Rebin(int xbin, double xmin, double xmax, int ybin, double ymin, double ymax); void RebinY(int ybin, double ymin, double ymax); void SetChannelMap(bool onOff, int tickStep = 1) { isChannelMap = onOff; this->tickStep = tickStep;} void UpdatePlot(){ colorMap->rescaleDataRange(true); replot(); } void Clear(); // Clear Data and histrogram void Fill(double x, double y); void DrawCut(); void ClearAllCuts(); QList GetCutList() const{return cutList;} // this list may contain empty element QList GetCutEntryList() const{ return cutEntryList;} QList GetCutNameList() const { return cutNameList;} void PrintCutEntry() const; double GetXNBin() const {return xBin;} double GetXMin() const {return xMin;} double GetXMax() const {return xMax;} double GetYNBin() const {return yBin;} double GetYMin() const {return yMin;} double GetYMax() const {return yMax;} void SaveCuts(QString cutFileName); void LoadCuts(QString cutFileName); private: double xMin, xMax, yMin, yMax; int xBin, yBin; bool isChannelMap; int tickStep; bool isLogZ; QCPColorMap * colorMap; QCPColorScale *colorScale; bool usingMenu; int entry[3][3]; // overflow counter, entrt[1][1] is entry in the plot; QCPItemRect * box[3][3]; QCPItemText * txt[3][3]; QPolygonF tempCut; int tempCutID; // only incresing; int numCut; QList cutList; QList cutNameList; // name of the cut QList cutEntryList; // number of entry inside the cut. QList cutIDList; // ID of the cut QList cutTextIDList; // QList plottableIDList; bool isDrawCut; int lastPlottableID; QCPItemLine * line; double oldMouseX = 0.0, oldMouseY = 0.0; bool isBusy; void rightMouseClickMenu(QMouseEvent * event); void rightMouseClickRebin(); QString settingPath; }; //^############################################### //^############################################### inline Histogram2D::Histogram2D(QString title, QString xLabel, QString yLabel, int xbin, double xmin, double xmax, int ybin, double ymin, double ymax, QWidget * parent, QString defaultPath) : QCustomPlot(parent){ // DebugPrint("%s", "Histogram2D"); settingPath = defaultPath; for( int i = 0; i < 3; i ++ ){ for( int j = 0; j < 3; j ++ ){ box[i][j] = nullptr; txt[i][j] = nullptr; } } isChannelMap = false; tickStep = 1; // only used when isChannelMap = true isLogZ = false; axisRect()->setupFullAxesBox(true); xAxis->setLabel(xLabel); yAxis->setLabel(yLabel); colorMap = new QCPColorMap(xAxis, yAxis); Rebin(xbin, xmin, xmax, ybin, ymin, ymax); colorMap->setInterpolate(false); QCPTextElement *titleEle = new QCPTextElement(this, title, QFont("sans", 12)); plotLayout()->insertRow(0); plotLayout()->addElement(0, 0, titleEle); colorScale = new QCPColorScale(this); plotLayout()->addElement(1, 1, colorScale); colorScale->setType(QCPAxis::atRight); colorMap->setColorScale(colorScale); QCPColorGradient color; color.setNanHandling(QCPColorGradient::NanHandling::nhNanColor); color.setNanColor(QColor(0,0,0,0)); color.clearColorStops(); // color.setColorStopAt( 0.0, QColor("white" )); color.setColorStopAt( 0.0, QColor("purple" )); color.setColorStopAt( 0.2, QColor("blue")); color.setColorStopAt( 0.4, QColor("cyan")); color.setColorStopAt( 0.6, QColor("green")); color.setColorStopAt( 0.8, QColor("yellow")); color.setColorStopAt( 1.0, QColor("red")); colorMap->setGradient(color); double xPosStart = 0.02; double xPosStep = 0.07; double yPosStart = 0.02; double yPosStep = 0.05; for( int i = 0; i < 3; i ++ ){ for( int j = 0; j < 3; j ++ ){ box[i][j] = new QCPItemRect(this); box[i][j]->topLeft->setType(QCPItemPosition::ptAxisRectRatio); box[i][j]->topLeft->setCoords(xPosStart + xPosStep*i, yPosStart + yPosStep*j); box[i][j]->bottomRight->setType(QCPItemPosition::ptAxisRectRatio); box[i][j]->bottomRight->setCoords(xPosStart + xPosStep*(i+1), yPosStart + yPosStep*(j+1)); txt[i][j] = new QCPItemText(this); txt[i][j]->setPositionAlignment(Qt::AlignLeft); txt[i][j]->position->setType(QCPItemPosition::ptAxisRectRatio); txt[i][j]->position->setCoords(xPosStart + xPosStep/2 + xPosStep*i, yPosStart + yPosStep*j);; txt[i][j]->setText("0"); txt[i][j]->setFont(QFont("Helvetica", 9)); } } cutList.clear(); cutEntryList.clear(); rescaleAxes(); usingMenu = false; isDrawCut = false; tempCutID = -1; numCut = 0; lastPlottableID = -1; line = new QCPItemLine(this); line->setPen(QPen(Qt::gray, 1, Qt::DashLine)); line->setVisible(false); isBusy = false; connect(this, &QCustomPlot::mouseMove, this, [=](QMouseEvent *event){ double x = xAxis->pixelToCoord(event->pos().x()); double y = yAxis->pixelToCoord(event->pos().y()); int xI, yI; colorMap->data()->coordToCell(x, y, &xI, &yI); double z = colorMap->data()->cell(xI, yI); QString coordinates = QString("X: %1, Y: %2, Z: %3").arg(x).arg(y).arg(z); QToolTip::showText(event->globalPosition().toPoint(), coordinates, this); //when drawing cut, show dashhed line if( isDrawCut && tempCut.size() > 0 ){ line->end->setCoords(x,y); line->setVisible(true); replot(); } }); connect(this, &QCustomPlot::mousePress, this, [=](QMouseEvent * event){ if (event->button() == Qt::LeftButton && !usingMenu && !isDrawCut){ setSelectionRectMode(QCP::SelectionRectMode::srmZoom); } if (event->button() == Qt::LeftButton && isDrawCut){ oldMouseX = xAxis->pixelToCoord(event->pos().x()); oldMouseY = yAxis->pixelToCoord(event->pos().y()); tempCut.push_back(QPointF(oldMouseX,oldMouseY)); line->start->setCoords(oldMouseX, oldMouseY); line->end->setCoords(oldMouseX, oldMouseY); line->setVisible(true); DrawCut(); } //^================= right click if (event->button() == Qt::RightButton) rightMouseClickMenu(event); }); //connect( this, &QCustomPlot::mouseDoubleClick, this, [=](QMouseEvent *event){ connect( this, &QCustomPlot::mouseDoubleClick, this, [=](){ if( isDrawCut) { tempCut.push_back(tempCut[0]); DrawCut(); isDrawCut = false; line->setVisible(false); plottableIDList.push_back(plottableCount() -1 ); cutNameList.push_back("Cut-" + QString::number(cutList.count())); cutEntryList.push_back(0); QCPItemText * text = new QCPItemText(this); text->setText(cutNameList.last()); text->position->setCoords(tempCut[0].rx(), tempCut[0].ry()); int colorID = tempCutID% colorCycle.count(); text->setColor(colorCycle[colorID].first); cutTextIDList.push_back(itemCount() - 1); replot(); cutList.push_back(tempCut); cutIDList.push_back(tempCutID); // qDebug() << "----------- end of create cut"; // qDebug() << " cutIDList " << cutIDList ; // qDebug() << "plottableIDList " << plottableIDList << ", " << plottableCount(); // qDebug() << " cutTextIDList " << cutTextIDList << ", " << itemCount(); } }); connect(this, &QCustomPlot::mouseRelease, this, [=](){ }); } inline void Histogram2D::Fill(double x, double y){ // DebugPrint("%s", "Histogram2D"); if( isBusy ) return; int xIndex, yIndex; colorMap->data()->coordToCell(x, y, &xIndex, &yIndex); //printf("%f, %d %d| %f, %d %d\n", x, xIndex, xBin, y, yIndex, yBin); int xk = 1, yk = 1; if( xIndex < 0 ) xk = 0; if( xIndex >= xBin ) xk = 2; if( yIndex < 0 ) yk = 2; if( yIndex >= yBin ) yk = 0; entry[xk][yk] ++; txt[xk][yk]->setText(QString::number(entry[xk][yk])); if( xk == 1 && yk == 1 ) { double value = colorMap->data()->cell(xIndex, yIndex); if( std::isnan(value) ){ colorMap->data()->setCell(xIndex, yIndex, 1); }else{ colorMap->data()->setCell(xIndex, yIndex, value + 1); } for( int i = 0; i < cutList.count(); i++){ if( cutList[i].isEmpty() ) continue; if( cutList[i].containsPoint(QPointF(x,y), Qt::OddEvenFill) ) cutEntryList[i] ++; } } } inline void Histogram2D::Rebin(int xbin, double xmin, double xmax, int ybin, double ymin, double ymax){ // DebugPrint("%s", "Histogram2D"); xMin = xmin; xMax = xmax; yMin = ymin; yMax = ymax; xBin = xbin + 2; yBin = ybin + 2; if( xBin > 1002) xBin = 1002; if( yBin > 1002) yBin = 1002; colorMap->data()->clear(); colorMap->data()->setSize(xBin, yBin); colorMap->data()->setRange(QCPRange(xMin, xMax), QCPRange(yMin, yMax)); for( int i = 0; i < xBin; i++){ for( int j = 0; j < yBin; j++){ colorMap->data()->setCell(i, j, NAN); } } if( isChannelMap ){ QCPAxis * xAxis = colorMap->keyAxis(); xAxis->ticker()->setTickCount(xbin/tickStep); xAxis->ticker()->setTickOrigin(0); } for( int i = 0; i < 3; i ++){ for( int j = 0; j < 3; j ++){ entry[i][j] = 0; if( txt[i][j] ) txt[i][j]->setText("0"); } } rescaleAxes(); UpdatePlot(); } inline void Histogram2D::RebinY(int ybin, double ymin, double ymax){ Rebin(xBin-2, xMin, xMax, ybin, ymin, ymax); } inline void Histogram2D::Clear(){ DebugPrint("%s", "Histogram2D"); for( int i = 0; i < 3; i ++){ for( int j = 0; j < 3; j ++){ entry[i][j] = 0; txt[i][j]->setText("0"); } } colorMap->data()->clear(); colorMap->data()->setSize(xBin, yBin); colorMap->data()->setRange(QCPRange(xMin, xMax), QCPRange(yMin, yMax)); for( int i = 0; i < xBin; i++){ for( int j = 0; j < yBin; j++){ colorMap->data()->setCell(i, j, NAN); } } UpdatePlot(); } inline void Histogram2D::ClearAllCuts(){ DebugPrint("%s", "Histogram2D"); numCut = 0; tempCutID = -1; lastPlottableID = -1; cutList.clear(); cutIDList.clear(); for( int i = cutTextIDList.count() - 1; i >= 0 ; i--){ if( cutTextIDList[i] < 0 ) continue; removeItem(cutTextIDList[i]); removePlottable(plottableIDList[i]); } replot(); cutTextIDList.clear(); plottableIDList.clear(); cutNameList.clear(); cutEntryList.clear(); } inline void Histogram2D::PrintCutEntry() const{ DebugPrint("%s", "Histogram2D"); if( numCut == 0 ) return; printf("=============== There are %d cuts. (%lld, %lld)\n", numCut, cutList.count(), cutEntryList.count()); for( int i = 0; i < cutList.count(); i++){ if( cutList[i].isEmpty() ) continue; printf("%10s | %d \n", cutNameList[i].toStdString().c_str(), cutEntryList[i]); } } inline void Histogram2D::DrawCut(){ DebugPrint("%s", "Histogram2D"); //The histogram is the 1st plottable. // the lastPlottableID should be numCut+ 1 if( lastPlottableID != numCut ){ removePlottable(lastPlottableID); } if(tempCut.size() > 0) { QCPCurve *polygon = new QCPCurve(xAxis, yAxis); lastPlottableID = plottableCount() - 1; int colorID = tempCutID% colorCycle.count(); QPen pen(colorCycle[colorID].first); pen.setWidth(1); polygon->setPen(pen); QVector dataPoints; for (const QPointF& point : tempCut) { dataPoints.append(QCPCurveData(dataPoints.size(), point.x(), point.y())); } polygon->data()->set(dataPoints, false); } replot(); // qDebug() << "Plottable count : " << plottableCount() << ", cutList.count :" << cutList.count() << ", cutID :" << lastPlottableID; } inline void Histogram2D::rightMouseClickMenu(QMouseEvent * event){ DebugPrint("%s", "Histogram2D"); usingMenu = true; setSelectionRectMode(QCP::SelectionRectMode::srmNone); QMenu *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); QAction * a1 = menu->addAction("UnZoom"); QAction * a6 = menu->addAction("Set/UnSet Log-Z"); QAction * a2 = menu->addAction("Clear hist."); QAction * a3 = menu->addAction("Toggle Stat."); QAction * a4 = menu->addAction("Rebin (clear histogram)"); QAction * a8 = menu->addAction("Load Cut(s)"); QAction * a5 = menu->addAction("Create a Cut"); QAction * b0 = nullptr; QAction * b1 = nullptr; QAction * b2 = nullptr; if( numCut > 0 ) { menu->addSeparator(); b0 = menu->addAction("Save Cut(s)"); b2 = menu->addAction("Add/Edit names to Cuts"); b1 = menu->addAction("Clear all Cuts"); } for( int i = 0; i < cutList.size(); i++){ if( cutList[i].isEmpty()) continue; QString haha = ""; menu->addAction("Delete " + cutNameList[i] + " ["+ colorCycle[cutIDList[i]%colorCycle.count()].second+"]"); } QAction *selectedAction = menu->exec(event->globalPosition().toPoint()); // qDebug() << "======================="; // qDebug() << selectedAction; // qDebug() << b2; if( selectedAction == nullptr ){ usingMenu = false; return; } if( selectedAction == a1 ){ xAxis->setRangeLower(xMin); xAxis->setRangeUpper(xMax); yAxis->setRangeLower(yMin); yAxis->setRangeUpper(yMax); replot(); } if( selectedAction == a2 ) { Clear(); } if( selectedAction == a3 ){ for( int i = 0; i < 3; i ++ ){ for( int j = 0; j < 3; j ++ ){ box[i][j]->setVisible( !box[i][j]->visible()); txt[i][j]->setVisible( !txt[i][j]->visible()); } } replot(); } if( selectedAction == a4){ rightMouseClickRebin(); } if( selectedAction == a5 ){ tempCut.clear(); tempCutID ++; isDrawCut= true; numCut ++; } if( selectedAction == a6){ if( !isLogZ ){ colorMap->setDataScaleType(QCPAxis::stLogarithmic); isLogZ = true; }else{ colorMap->setDataScaleType(QCPAxis::stLinear); isLogZ = false; } replot(); } if( selectedAction == a8 ){ // load Cuts QString filePath = QFileDialog::getOpenFileName(this, "Load Cuts from File", settingPath, "Text file (*.txt)"); if (!filePath.isEmpty()) LoadCuts(filePath); } //*==================================== when there are cuts if( selectedAction == b0 ){ // Save Cuts QString filePath = QFileDialog::getSaveFileName(this, "Save Cuts to File", settingPath, "Text file (*.txt)"); if (!filePath.isEmpty()) SaveCuts(filePath); } if( selectedAction == b1 ){ ClearAllCuts(); } if( selectedAction == b2 ){ QDialog dialog(this); dialog.setWindowTitle("Add/Edit name of cuts "); QFormLayout layout(&dialog); for(int i = 0; i < cutTextIDList.count(); i++){ if( cutTextIDList[i] < 0 ) continue; QLineEdit * le = new QLineEdit(&dialog); layout.addRow(colorCycle[i%colorCycle.count()].second, le); le->setText( cutNameList[i] ); connect(le, &QLineEdit::textChanged, this, [=](){ le->setStyleSheet("color : blue;"); }); connect(le, &QLineEdit::returnPressed, this, [=](){ le->setStyleSheet(""); cutNameList[i] = le->text(); ((QCPItemText *) this->item(cutTextIDList[i]))->setText(le->text()); replot(); }); } dialog.exec(); } if( selectedAction && numCut > 0 && selectedAction->text().contains("Delete ") ){ QString haha = selectedAction->text(); int index1 = haha.indexOf("-"); int index2 = haha.indexOf("["); int cutID = haha.mid(index1+1, index2-index1-1).remove(' ').toInt(); removeItem(cutTextIDList[cutID]); removePlottable(plottableIDList[cutID]); replot(); numCut --; cutList[cutID].clear(); cutIDList[cutID] = -1; cutTextIDList[cutID] = -1; plottableIDList[cutID] = -1; cutNameList[cutID] = ""; cutEntryList[cutID] = -1; for( int i = cutID + 1; i < cutTextIDList.count() ; i++){ cutTextIDList[i] --; plottableIDList[i] --; } if( numCut == 0 ){ tempCutID = -1; lastPlottableID = -1; cutList.clear(); cutIDList.clear(); cutTextIDList.clear(); plottableIDList.clear(); cutNameList.clear(); cutEntryList.clear(); } } usingMenu = false; } inline void Histogram2D::rightMouseClickRebin(){ DebugPrint("%s", "Histogram2D"); QDialog dialog(this); dialog.setWindowTitle("Rebin histogram"); QFormLayout layout(&dialog); QLabel * info = new QLabel(&dialog); info->setStyleSheet("color:red;"); info->setText("This will also clear histogram!!"); layout.addRow(info); QStringList nameListX = {"Num. x-Bin", "x-Min", "x-Max"}; QLineEdit* lineEditX[3]; for (int i = 0; i < 3; ++i) { lineEditX[i] = new QLineEdit(&dialog); layout.addRow(nameListX[i] + " : ", lineEditX[i]); } lineEditX[0]->setText(QString::number(xBin-2)); lineEditX[1]->setText(QString::number(xMin)); lineEditX[2]->setText(QString::number(xMax)); QStringList nameListY = {"Num. y-Bin", "y-Min", "y-Max"}; QLineEdit* lineEditY[3]; for (int i = 0; i < 3; ++i) { lineEditY[i] = new QLineEdit(&dialog); layout.addRow(nameListY[i] + " : ", lineEditY[i]); } lineEditY[0]->setText(QString::number(yBin-2)); lineEditY[1]->setText(QString::number(yMin)); lineEditY[2]->setText(QString::number(yMax)); QLabel * msg = new QLabel(&dialog); msg->setStyleSheet("color:red;"); layout.addRow(msg); QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); layout.addRow(&buttonBox); double number[3][2]; QObject::connect(&buttonBox, &QDialogButtonBox::accepted, [&]() { int OKcount = 0; bool conversionOk = true; for( int i = 0; i < 3; i++ ){ number[i][0] = lineEditX[i]->text().toDouble(&conversionOk); if( conversionOk ){ OKcount++; }else{ msg->setText(nameListX[i] + " is invalid."); return; } } for( int i = 0; i < 3; i++ ){ number[i][1] = lineEditY[i]->text().toDouble(&conversionOk); if( conversionOk ){ OKcount++; }else{ msg->setText(nameListY[i] + " is invalid."); return; } } if( OKcount == 6 ) { if( number[0][0] <= 0 ) { msg->setText( nameListX[0] + " is zero or negative" ); return; } if( number[0][0] <= 0 ) { msg->setText( nameListX[0] + " is zero or negative" ); return; } if( number[2][0] > number[1][0] && number[2][1] > number[1][1] ) { dialog.accept(); }else{ if( number[2][0] > number[1][0] ){ msg->setText(nameListX[2] + " is smaller than " + nameListX[1]); } if( number[2][1] > number[1][1] ){ msg->setText(nameListY[2] + " is smaller than " + nameListY[1]); } } } }); QObject::connect(&buttonBox, &QDialogButtonBox::rejected, [&]() { dialog.reject();}); if( dialog.exec() == QDialog::Accepted ){ isBusy = true; Rebin((int)number[0][0], number[1][0], number[2][0], (int)number[0][1], number[1][1], number[2][1]); isBusy = false; } } inline void Histogram2D::SaveCuts(QString cutFileName){ QFile file(cutFileName); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&file); // Define the text to write QStringList lines; for( int i = 0; i < cutList.size(); i++){ lines << "====== "+ cutNameList[i]; for( int pt = 0 ; pt < cutList[i].size(); pt ++){ lines << QString::number(cutList[i][pt].rx(), 'g', 5) + "," + QString::number(cutList[i][pt].ry(), 'g', 5); } } lines << "#===== End of File"; // Write each line to the file for (const QString &line : lines) out << line << "\n"; // Close the file file.close(); qDebug() << "File written successfully to" << cutFileName; }else{ qWarning() << "Unable to open file" << cutFileName; } } inline void Histogram2D::LoadCuts(QString cutFileName){ QFile file(cutFileName); QString cutNameTemp; // Open the file in read mode if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in(&file); ClearAllCuts(); tempCut.clear(); // Read each line and append to the QStringList while (!in.atEnd()) { QString line = in.readLine(); if( line.contains("======") ){ if( !tempCut.isEmpty() ) { DrawCut(); plottableIDList.push_back(plottableCount() -1 ); cutNameList.push_back(cutNameTemp); cutEntryList.push_back(0); QCPItemText * text = new QCPItemText(this); text->setText(cutNameList.last()); text->position->setCoords(tempCut[0].rx(), tempCut[0].ry()); int colorID = tempCutID% colorCycle.count(); text->setColor(colorCycle[colorID].first); cutTextIDList.push_back(itemCount() - 1); cutList.push_back(tempCut); cutIDList.push_back(tempCutID); } tempCut.clear(); tempCutID ++; numCut ++; int spacePos = line.indexOf(' '); cutNameTemp = line.mid(spacePos + 1); continue; } if( line.contains("#==") ) { DrawCut(); plottableIDList.push_back(plottableCount() -1 ); cutNameList.push_back(cutNameTemp); cutEntryList.push_back(0); QCPItemText * text = new QCPItemText(this); text->setText(cutNameList.last()); text->position->setCoords(tempCut[0].rx(), tempCut[0].ry()); int colorID = tempCutID% colorCycle.count(); text->setColor(colorCycle[colorID].first); cutTextIDList.push_back(itemCount() - 1); cutList.push_back(tempCut); cutIDList.push_back(tempCutID); break; }else{ QStringList haha = line.split(","); // qDebug() << haha; if( haha.size() == 2 ){ tempCut.push_back(QPointF(haha[0].toFloat(), haha[1].toFloat())); DrawCut(); } } } // Close the file file.close(); qDebug() << "File read successfully from" << cutFileName; qDebug() << " Number of cut loaded " << numCut << ", " << cutList.count(); // PrintCutEntry(); // DrawCut(); replot(); } else { qWarning() << "Unable to open file" << cutFileName; } } #endif