QCustomplot drawing chart with sharing - multi function cursor

Keywords: C++ Qt Mobile less

Catalog

Original link: QCustomplot use sharing (IX) to draw chart - multi function cursor

I. overview

Last article QCustomplot use sharing (VIII) layer (end) This paper describes the use of QCustomPlot control in the first part, which mainly shows the multi-dimensional line chart, and has a simple cursor display effect. This article is a function enhancement based on the previous article. It mainly focuses on cursor optimization and provides more rich cursor functions.

II. Renderings

As shown in the figure below, it's a test rendering I made, including a simple line graph and a series of cursors on the way. The display mode of the line graph has more than ten effects, which can be seen in detail. What can QCustomplot do with sharing (I) I'm not posting the screenshot of this article here.

This rendering mainly shows the use of cursors. Other related functions can refer to the previous articles. At the end of this article, it will also be provided through the relevant articles section. Interested students can go to the end of the article to find.

The data in demo is read from cvs file. If you want to get the data from other channels, you can also. This drawing control has added enough interfaces for calling.

The cursor functions provided by the drawing control are as follows, for example:

  1. Multiple types of cursors, single cursors, double cursors
  2. Cursor display, hide, support moving
  3. Double cursor lock move, non lock move
  4. Get cursor interval value
  5. Set cursor color
  6. Get cursor interval data

In the following article, I will analyze the main interface and core function implementation

The code of the effect test shown in the figure is as follows, and there are two key nodes in the code.

  1. Construct the escvsdboperator class and load the cvs file
  2. Set data through set interface and set line chart type
ESCsvDBOperater * csvDBOperater = new ESCsvDBOperater(nullptr);
csvDBOperater->loadCSVFile(qApp->applicationDirPath() + "\\temp\\test31.csv");
QStringList names = csvDBOperater->getCSVNames();
auto callback = [this, names](const QString & name, const QVector<double> & data){
    int index = names.indexOf(name);
    if (index != -1)
    {
        if (index == 0)
        {
            ui->widget->SetGraphKey(data);
        }
        else
        {
            int l = name.indexOf("(");
            int r = name.indexOf(")");
            if (l != -1 && r != -1)
            {
                ui->widget->SetGraphValue(index - 1, name.left(l), /*name.mid(l + 1, r - l - 1)*/"", data);
                ui->widget->SetGraphScatterStyle(index - 1, 4);
            }
            else
            {
                ui->widget->SetGraphValue(index - 1, name, "", data);
            }
        }
    }

Of course, QCP can not only display line graphs, but also display various renderings. What can QCustomplot do with sharing (I) Watch in the article

III. source code explanation

1. Source code structure

As shown in the figure, it's a screenshot of the project's header file. The number of files in the figure is relatively large, but we may only use an ESMPMultiPlot class for external use. This class provides many interfaces, which is enough for us to use. Of course, if there are special needs, it can be customized.

2. Header file

The following are the interfaces in the header file. I just listed the relevant Public interfaces. These interfaces are also the ones we use more often. You should know what the interface is by looking at the interface name, so I won't go into details.

void ClearCache();//Clear previous csv plot data
void SetGraphCount(int);//Set the number of line graphs
void SetGraphKey(const QVector<double> &);//Set x-axis data
void SetGraphKeyRange(double, double);//Set x-axis range, i.e. time range
void SetGraphScatterStyle(int, int);//Set line style

void SetGraphValue(int, const QString &, const QString &
    , const QVector<double> &);//Set line chart data
void AppendGraphValue(int, double, double);//Add line chart data
void AppendGraphValue(int, const QVector<double> &, const QVector<double> &);//Add line chart data

QVector<double> GetGraphValues(int, int);//Get the cursor interval value parameter 1 of line chart: Line subscript parameter 2: cursor order

QString GetGraphName(int) const;
void SetGraphColor(int, const QColor &);//Set line color
QColor GetGraphColor(int);//Get line color

void SetSingleCursor(bool single);//Start single cursor
bool IsSingleCursor(int index) const;//Test whether the cursor is a single cursor
void ShowCursor(bool visible = true);//Set whether the cursor displays
void AppendCursor(const QColor & color);//New cursor
void LockedCursor(int, bool);//Lock specifies whether the cursor parameter 2 is locked
int CursorCount() const;
bool CursorVisible() const;//Whether the cursor displays
void SetCursorColor(int index, const QColor &);//Set cursor color the second parameter indicates which cursor
double GetCursorKey(bool);//Get the x-axis value of cursor object true indicates the left cursor false indicates the right cursor
double GetCursorKey(int index, bool);//Get the x-axis value of cursor object true indicates the left cursor false indicates the right cursor

void ResizeKeyRange(bool, int index = 0);//Set x-axis scaling true and press cursor scaling false to restore the default state
void ResizeValueRange();//y-axis adaptive

void ConfigureGraph();//Set up
void ConfigureGraphAmplitude(int);//Triggered when double clicking the right unit
void SavePng(const QString & = "");//Save the picture 1. Automatically execute the analysis and transfer it to the path 2. Click the save graph button to transfer to the empty path

3. Add cursor

The following is the code to simulate adding cursors. A variable i is used to simulate different situations and add different types of cursors. Currently, it supports adding movable single cursors, movable double cursors and lockable drag double cursors.

  1. Single cursor: single thread can be dragged by mouse
  2. Movable double cursors: two lines, moving separately, the left cursors will never be larger than the right cursors
  3. Lockable drag double cursors: two lines, lock move, that is to say, no matter which line is moved, the other line will move synchronously and remain in the window
void ESMultiPlot::on_pushButton_add_cursor_clicked()
{
    graphColor.append(Qt::red);
    graphColor.append(Qt::green);
    graphColor.append(Qt::blue);
    graphColor.append(Qt::gray);
    graphColor.append(Qt::cyan);
    graphColor.append(Qt::yellow);
    graphColor.append(Qt::magenta);

    static int i = 1;

    if (i % 3 == 0)
    {
        ui->widget->SetSingleCursor(true);
        ui->widget->AppendCursor(graphColor[rand() % 6 + 1]);
    }
    else if (i % 3 == 1)
    {
        ui->widget->SetSingleCursor(false);
        ui->widget->AppendCursor(graphColor[rand() % 6 + 1]);
        ui->widget->LockedCursor(i, false);
    }
    else
    {
        ui->widget->SetSingleCursor(false);
        ui->widget->AppendCursor(graphColor[rand() % 6 + 1]);
        ui->widget->LockedCursor(i, true);
    }
    ++i;
}

As shown in the above code, when setsingcursor is set to true, it means that the cursor to be added next is a single cursor; LockedCursor can lock the specified double cursors, which is not effective for a single cursor.

4. Monitoring movement

In multi cursor mode, moving cursors is more complex than a group of cursors. We need to loop through all cursors and get a movable cursor.

Here, the logic to get the moving cursor is to get the cursor within 5 pixels from the mouse down position, and give priority to the cursor constructed first, and give priority to the right cursor if the left and right cursors are satisfied at the same time

void ESMPMultiPlot::mousePressEvent(QMouseEvent * event)
{
    if (m_bCursor)
    {
        m_bDrag = true;
        for (int i = 0; i < m_pCursors.size(); ++i)
        {
            QCPItemStraightLine * leftCursor = m_pCursors.at(i).leftCursor;
            bool ispressed = false;
            double distance = leftCursor->selectTest(event->pos(), false);
            if (distance <= 5 && axisRect()->rect().contains(event->pos()))
            {
                m_bDragType = 1;
                m_bLeftCursor = true;
                ispressed = true;
                m_bLock = m_pCursors.at(i).lock;
                m_bSingleCursor = m_pCursors.at(i).single;
                m_bOrder = i;
            }

            QCPItemStraightLine * rightCursor = m_pCursors.at(i).rightCursor;
            distance = rightCursor->selectTest(event->pos(), false);
            if (distance <= 5 && axisRect()->rect().contains(event->pos()))
            {
                m_bDragType = 1;
                m_bLeftCursor = false;
                ispressed = true;
                m_bLock = m_pCursors.at(i).lock;
                m_bSingleCursor = m_pCursors.at(i).single;
                m_bOrder = i;
            }
            if (ispressed)
            {
                break;
            }
        }
    }

    for (int i = 0; i < m_vecNames.size(); ++i)
    {
        double distance = m_vecNames[i]->selectTest(event->pos(), false);
        //QPointF posF = m_vecNames[i]->position->pixelPosition;
        if (distance <= 13 && m_vecNames[i]->visible())
        {
            m_bDragType = 2;
            m_iDragIndex = i;
            break;
        }
    }

    __super::mousePressEvent(event);
}

5. Move cursor

QCustomplot use sharing (VIII) layer (end) This article describes a group of cursor movements, which need to consider less points when moving the cursor, respectively:

  1. Cursor cannot move out of interface
  2. Left cursor must be smaller than right cursor

In this article, more points need to be considered for multiple cursor movements, respectively:

Cursor default cursor group (one or two cursors); left and right cursors are for two cursors

Basic rules

  1. Cursor cannot move out of interface

Single cursor

  1. When the left side is a double cursor, it is compared with the left right cursor, and vice versa.
  2. Right direct to left cursor ratio

Double cursor unlocked - move left cursor

  1. When the left side is a double cursor, it is compared with the left right cursor, and vice versa.
  2. Right direct to right cursor ratio

Double cursor unlocked - move right cursor

  1. Right direct to right cursor left cursor ratio
  2. Left direct to left cursor ratio

Double cursor lock

  1. When moving right, directly use the left cursor ratio of the right cursor and the right cursor
  2. When moving left, directly use the right cursor ratio of the left cursor and the left cursor.

As shown in the following code, it is the core code of the mobile cursor. The main mobile situation has been mentioned above. The mobile logic at the bottom is not detailed. Interested students can view it by themselves and add my QQ if they need to provide customized ones.

void ESMPMultiPlot::mouseMoveEvent(QMouseEvent * event)
{
if (m_bDragType == 1 && m_bDrag)
    {
        double pixelx = event->pos().x();
        QCPRange keyRange = axisRect()->axis(QCPAxis::atBottom)->range();
        double min = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(keyRange.lower);
        double max = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(keyRange.upper);

        if (min + 1 > pixelx)
        {
            pixelx = min + 1;
        }
        else if (max - 1 < pixelx)
        {
            pixelx = max - 1;
        }

        //Press and hold the left cursor to move
        double move_distance = 0;
        double rcursor = m_pCursors[m_bOrder].rightCursor->point1->key();
        double rcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(rcursor);

        double lcursor = m_pCursors[m_bOrder].leftCursor->point1->key();
        double lcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(lcursor);

        if (m_bLeftCursor)
        {
            //Corrected left
            if (m_bOrder != 0)
            {
                double rcursor;
                if (m_pCursors[m_bOrder - 1].rightCursor->visible())
                {
                    rcursor = m_pCursors[m_bOrder - 1].rightCursor->point1->key();
                }
                else//Single cursor on the left
                {
                    rcursor = m_pCursors[m_bOrder - 1].leftCursor->point1->key();
                }
                double rcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(rcursor);

                if (pixelx <= rcursorx + 4)
                {
                    pixelx = rcursorx + 4;
                }
                move_distance = rcursorx - pixelx;//Leftable distance (negative to left)
            }
            else
            {
                if (pixelx <= min + 2)
                {
                    pixelx = min + 2;
                }
                move_distance = min - pixelx;//Move left (negative left)
            }

            //Right correction
            if (m_bLock)//Lock Mobile
            {
                move_distance = pixelx - lcursorx;//Distance to the right to move

                if (m_bOrder == m_pCursors.size() - 1)
                {
                    if (rcursorx + move_distance > max - 2)
                    {
                        move_distance = max - 2 - rcursorx;//True moving distance to the right
                    }
                }
                else
                {
                    double nlcursor = m_pCursors[m_bOrder + 1].leftCursor->point1->key();
                    double nlcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(nlcursor);
                    if (rcursorx + move_distance > nlcursorx - 4)
                    {
                        move_distance = nlcursorx - 4 - rcursorx;//True moving distance to the right
                    }
                }
            }
            else
            {
                if (m_bSingleCursor)
                {
                    move_distance = pixelx - lcursorx;//Distance to the right to move

                    if (m_bOrder == m_pCursors.size() - 1)
                    {
                        if (lcursorx + move_distance > max - 2)
                        {
                            move_distance = max - 2 - lcursorx;//True moving distance to the right
                        }
                    }
                    else
                    {
                        double nlcursor = m_pCursors[m_bOrder + 1].leftCursor->point1->key();
                        double nlcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(nlcursor);
                        if (lcursorx + move_distance > nlcursorx - 4)
                        {
                            move_distance = nlcursorx - 4 - lcursorx;//True moving distance to the right
                        }
                    }
                }
                else
                {
                    if (pixelx >= rcursorx - 4)
                    {
                        pixelx = rcursorx - 4;
                    }
                    move_distance = pixelx - lcursorx;//Moving distance to the right (positive to the right)
                }
            }
        }
        else//Press and hold the right cursor to move
        {
            //Right correction
            if (m_bOrder != m_pCursors.size() - 1)
            {
                double lcursor = m_pCursors[m_bOrder + 1].leftCursor->point1->key();
                double lcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(lcursor);

                if (pixelx >= lcursorx - 4)
                {
                    pixelx = lcursorx - 4;
                }
                move_distance = pixelx - lcursorx;//Move right
            }
            else
            {
                if (pixelx >= max - 2)
                {
                    pixelx = max - 2;
                }
                move_distance = pixelx - lcursorx;//Move right
            }

            //Corrected left
            if (m_bLock)//Lock Mobile
            {
                move_distance = pixelx - rcursorx;//The distance to the left to move
                if (m_bOrder == 0)
                {
                    if (lcursorx + move_distance <= min + 2)
                    {
                        move_distance = min + 2 - lcursorx;//True left moving distance
                    }
                }
                else
                {
                    double nlcursor = m_pCursors[m_bOrder - 1].rightCursor->point1->key();
                    double nlcursorx = axisRect()->axis(QCPAxis::atBottom)->coordToPixel(nlcursor);
                    if (lcursorx + move_distance <= nlcursorx + 4)
                    {
                        move_distance = nlcursorx + 4 - lcursorx;//True moving distance to the right
                    }
                }
            }
            else
            {
                if (pixelx <= lcursorx + 4)
                {
                    pixelx = lcursorx + 4;
                }
                move_distance = pixelx - rcursorx;//Move left (negative left)
            }
        }

        double key;
        if (m_bLeftCursor)
        {
            key = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(lcursorx + move_distance);
            m_pCursors[m_bOrder].leftCursor->point1->setCoords(key, m_pCursors[m_bOrder].leftCursor->point1->value());
            m_pCursors[m_bOrder].leftCursor->point2->setCoords(key, m_pCursors[m_bOrder].leftCursor->point2->value());
        }
        else
        {
            key = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(rcursorx + move_distance);
            m_pCursors[m_bOrder].rightCursor->point1->setCoords(key, m_pCursors[m_bOrder].rightCursor->point1->value());
            m_pCursors[m_bOrder].rightCursor->point2->setCoords(key, m_pCursors[m_bOrder].rightCursor->point2->value());
        }

        if (m_bLock)
        {
            if (m_bLeftCursor)
            {
                key = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(rcursorx + move_distance);
                m_pCursors[m_bOrder].rightCursor->point1->setCoords(key, m_pCursors[m_bOrder].rightCursor->point1->value());
                m_pCursors[m_bOrder].rightCursor->point2->setCoords(key, m_pCursors[m_bOrder].rightCursor->point2->value());
            }
            else
            {
                key = axisRect()->axis(QCPAxis::atBottom)->pixelToCoord(lcursorx + move_distance);
                m_pCursors[m_bOrder].leftCursor->point1->setCoords(key, m_pCursors[m_bOrder].leftCursor->point1->value());
                m_pCursors[m_bOrder].leftCursor->point2->setCoords(key, m_pCursors[m_bOrder].leftCursor->point2->value());
            }
        }

        event->accept();
        replot();

        emit CursorChanged(m_bLeftCursor);
        return;
    }
    else if (m_bDragType == 2)
    {
        double pixely = event->pos().y();
        QCPRange keyRange = axisRect()->axis(QCPAxis::atLeft)->range();
        double max = axisRect()->axis(QCPAxis::atLeft)->coordToPixel(keyRange.lower);
        double min = axisRect()->axis(QCPAxis::atLeft)->coordToPixel(keyRange.upper);
        if (min > pixely)
        {
            pixely = min;
        }
        else if (max < pixely)
        {
            pixely = max;
        }

        m_vecNames[m_iDragIndex]->position->setType(QCPItemPosition::ptPlotCoords);
        double coordy1 = axisRect()->axis(QCPAxis::atLeft)->pixelToCoord(pixely);
        double coordx = m_vecNames[m_iDragIndex]->position->coords().rx();
        double coordy = m_vecNames[m_iDragIndex]->position->coords().ry();
        m_vecNames[m_iDragIndex]->position->setCoords(coordx, coordy1);

        m_vecUnits[m_iDragIndex]->position->setType(QCPItemPosition::ptPlotCoords);
        m_vecUnits[m_iDragIndex]->position->setCoords(m_vecUnits[m_iDragIndex]->position->coords().rx(), coordy1);
        
        (*m_graphConfigure)[m_iDragIndex].position += (coordy1 - coordy);

        RefrushGraph(m_iDragIndex);

        event->accept();
        replot();
        return;
    }

    __super::mouseMoveEvent(event);
}

In the ESMPPlot class, m ﹣ mapleftcursor and m ﹣ maprightcursor are left and right cursors respectively. Why does a map be taken here? The answer is: at that time, it was designed to support multiple vertically placed cursors to synchronize cursors. If the stock speculators might know that there might be a convenient line between the k line and the index. No matter which plot area they moved, the lines in another chart would also move with them.

It doesn't matter if you don't know this. Our control is a table by default, so there is only one finger in the map, so you can ignore this problem.

In the ESMPMultiPlot class, we simulate the function of esmpmplot. What about this time? There is only one rectangular coordinate axis. The x axis is the same, which represents time. For the y axis of different curves, we translate it to achieve different display positions.

There is a very important skill here, that is, we have made a unit conversion for the y-axis data, so that it can be better displayed in the area we set, maybe like the following

/*
    y1p=(y1-Yzero1)/Ygrid1+Xaxis1;% core conversion formula, convert the original coordinate value Y1 to the new coordinate value y1p
    y1;% original value
    Yzero1;% zero amplitude, the variable that determines the zero position of curve 1
    Ygrid1;% single cell amplitude, which determines the size of each cell of curve 1
    Xaxis1;% display position, the variable that determines the display position of curve 1 in the drawing board
*/

Of course, the coordinates we converted are just for display convenience. If we get the original values according to the UI, we need to use an inverse formula to convert them back.

6. Other functions

There are some other methods, such as saving the chart, getting the chart coordinates, setting the chart color, etc. we will not go into details here. The article is limited in length, and we can't post them all one by one. If you need a partner, you can contact me to provide function customization.

IV. test method

1. Test project

Control we will almost, here put out the test code, for your reference, first test project screenshot is shown below, most of our test code is written in the main function.






2. Test documents

In this case, the first column of Time represents the Time of the x-axis, while the data at the beginning of the second column are all our line graphs. A column of data represents a line graph, and the name of the column is the name on the left side of our line graph; the unit in the column name is the unit on the right side of the line graph.






3. Test code

Limited to space, here I still cut a lot of irrelevant code, you can contact me if you need complete source code.

void ESMPMultiPlot::LoadData()
{
    ESCsvDBOperater * csvDBOperater = new ESCsvDBOperater(nullptr);
    csvDBOperater->loadCSVFile(qApp->applicationDirPath() + "\\temp\\test31.csv");
    QStringList names = csvDBOperater->getCSVNames();

    auto callback = [this, names](const QString & name, const QVector<double> & data){
        //Add chart data
    };

    ui->widget->SetGraphCount(names.size() - 1);
    for (int i = 0; i < names.size(); ++i)
    {
        csvDBOperater->receiveData(names[i], callback);
    }

    double start = csvDBOperater->getStartTime();
    double end = csvDBOperater->getEndTime();

    csvDBOperater->receiveData(names[2], 10.201, 10.412, callback);
    QVector<double> tiems = csvDBOperater->getRangeTimeDatas(10.201, 10.412);

    ui->widget->SetGraphKeyRange(start, end);
}

V. related articles

  1. What can QCustomplot do with sharing (I)
  2. QCustomplot use and share (2) source code interpretation
  3. QCustomplot use sharing (3) diagram
  4. QCustomplot use sharing (IV) QCPAbstractItem
  5. QCustomplot use sharing (V) layout
  6. QCustomplot uses shared (six) axes and gridlines
  7. QCustomplot use sharing (7) layer (end)
  8. QCustomplot use sharing (VIII) layer (end)

Six, summary

QCustomPlot is a very powerful drawing class, and it is very efficient, and it can be used for programs with high efficiency requirements.

This article is the second use case after the previous 7 articles on QCP, and more complex functions will be provided in the future.

This control has been encapsulated into a dll by me. If you need a small partner, you can add me for consultation.

About beautification

Because my programs here are all test programs, so they are all original effects. If there are students or customers who need beautification, I can also provide customized beautification functions, welcome to consult.

If you think the article is good, you can give a reward. Writing is not easy. Thank you for your support. Your support is my biggest motivation, thank you!



Important - reprint statement

  1. This site article has no special explanation, all are original, all rights reserved, when reprint please use the link way, gives the original source. At the same time, write the original author: Ten nights to eight or Twowords

  2. If you want to reprint, please reprint the original text. If you want to modify this text during reprint, please inform in advance. You are not allowed to modify this text in order to benefit the reprinter.

Posted by burningkamikaze on Sat, 26 Oct 2019 13:07:47 -0700