QCustomPlot Discussion and Comments

How to get the point under the mouse in a graphReturn to overview

I hope this code helps others out, you should attach this slot to the plottable clicked signal provided by QCustomPlot. It will display a tool tip as long as the user holds the right mouse button down. I don't recommend using this function on a plot with hundreds of thousands of data points in a single graph because of the method by which it detects which point is the closest. A binary search can be used for the bar and regular graph plottables (because they have sorted data) but a linear search has to be used for curves... and curves have the most expensive search function.

void plotMousePress(QMouseEvent *event)
{
    if(event->button() == Qt::RightButton)
    {
        QCPAbstractPlottable *plottable =
        m_ui->plot->plottableAt(event->posF());

        if(plottable)
        {
            double x = m_ui->plot->xAxis->pixelToCoord(event->posF().x());
            double y = m_ui->plot->yAxis->pixelToCoord(event->posF().y());

            QCPBars *bar =
            qobject_cast<QCPBars*>(plottable);

            if(bar)
            {
                double key = 0;
                double value = 0;

                bool ok = false;
                double m = std::numeric_limits<double>::max();

                foreach(QCPBarData data, bar->data()->values())
                {
                    double d = qAbs(x - data.key);

                    if(d < m)
                    {
                        key = data.key;
                        value = data.value;

                        ok = true;
                        m = d;
                    }
                }

                if(ok)
                {
                    for(QCPBars *below = bar->barBelow();
                    ((below != NULL) && below->data()->contains(key));
                    below = below->barBelow())
                    {
                        value += below->data()->value(key).value;
                    }

                    QToolTip::hideText();
                    QToolTip::showText(event->globalPos(),
                    tr("<table>"
                         "<tr>"
                           "<th colspan=\"2\">%L1</th>"
                         "</tr>"
                         "<tr>"
                           "<td>Key:</td>" "<td>%L2</td>"
                         "</tr>"
                         "<tr>"
                           "<td>Val:</td>" "<td>%L3</td>"
                         "</tr>"
                       "</table>").
                       arg(bar->name().isEmpty() ? "..." : bar->name()).
                       arg(key).
                       arg(value),
                       m_ui->plot, m_ui->plot->rect());
                }
            }

            QCPCurve *curve =
            qobject_cast<QCPCurve*>(plottable);

            if(curve)
            {
                double key = 0;
                double value = 0;

                bool ok = false;
                double m = std::numeric_limits<double>::max();

                foreach(QCPCurveData data, curve->data()->values())
                {
                    double d = qSqrt(qPow(x-data.key,2)+qPow(y-data.value,2));

                    if(d < m)
                    {
                        key = data.key;
                        value = data.value;

                        ok = true;
                        m = d;
                    }
                }

                if(ok)
                {
                    QToolTip::hideText();
                    QToolTip::showText(event->globalPos(),
                    tr("<table>"
                         "<tr>"
                           "<th colspan=\"2\">%L1</th>"
                         "</tr>"
                         "<tr>"
                           "<td>X:</td>" "<td>%L2</td>"
                         "</tr>"
                         "<tr>"
                           "<td>Y:</td>" "<td>%L3</td>"
                         "</tr>"
                       "</table>").
                       arg(curve->name().isEmpty() ? "..." : curve->name()).
                       arg(key).
                       arg(value),
                       m_ui->plot, m_ui->plot->rect());
                }
            }

            QCPGraph *graph =
            qobject_cast<QCPGraph*>(plottable);

            if(graph)
            {
                double key = 0;
                double value = 0;

                bool ok = false;
                double m = std::numeric_limits<double>::max();

                foreach(QCPData data, graph->data()->values())
                {
                    double d = qAbs(x - data.key);

                    if(d < m)
                    {
                        key = data.key;
                        value = data.value;

                        ok = true;
                        m = d;
                    }
                }

                if(ok)
                {
                    QToolTip::hideText();
                    QToolTip::showText(event->globalPos(),
                    tr("<table>"
                         "<tr>"
                           "<th colspan=\"2\">%L1</th>"
                         "</tr>"
                         "<tr>"
                           "<td>X:</td>" "<td>%L2</td>"
                         "</tr>"
                         "<tr>"
                           "<td>Y:</td>" "<td>%L3</td>"
                         "</tr>"
                       "</table>").
                       arg(graph->name().isEmpty() ? "..." : graph->name()).
                       arg(key).
                       arg(value),
                       m_ui->plot, m_ui->plot->rect());
                }
            }

            QCPStatisticalBox *box =
            qobject_cast<QCPStatisticalBox*>(plottable);

            if(box)
            {
                double key = box->key();
                double minimum = box->minimum();
                double lowerQuartile = box->lowerQuartile();
                double median = box->median();
                double upperQuartile = box->upperQuartile();
                double maximum = box->maximum();

                QToolTip::hideText();
                QToolTip::showText(event->globalPos(),
                tr("<table>"
                     "<tr>"
                       "<th colspan=\"2\">%L1</th>"
                     "</tr>"
                     "<tr>"
                       "<td>Key:</td>" "<td>%L2</td>"
                     "</tr>"
                     "<tr>"
                       "<td>Min:</td>" "<td>%L3</td>"
                     "</tr>"
                     "<tr>"
                       "<td>L-Q:</td>" "<td>%L4</td>"
                     "</tr>"
                     "<tr>"
                       "<td>Mid:</td>" "<td>%L5</td>"
                     "</tr>"
                     "<tr>"
                       "<td>U-Q:</td>" "<td>%L6</td>"
                     "</tr>"
                     "<tr>"
                       "<td>Max:</td>" "<td>%L7</td>"
                     "</tr>"
                   "</table>").
                   arg(box->name().isEmpty() ? "..." : box->name()).
                   arg(key).
                   arg(minimum).
                   arg(lowerQuartile).
                   arg(median).
                   arg(upperQuartile).
                   arg(maximum),
                   m_ui->plot, m_ui->plot->rect());
            }
        }
    }
}

Thanks, Kwabena! I'm sure many people will find this helpful.

And it also tells me, that QCustomPlot should improve in that direction, to make this alot easier and spare the user all the fiddly work :)

This code pretty much gives you everything you need. Just re-implement the QToolTip Event in CustomPlot and run this code. Do a B-Search on the sorted data plottables... and you'e good. Also, provide a call back function for people who have custom plottables.

However, I still don't know how to get the tool tip to stay without having the hold the right mouse button down. But, the above is a good start.

Thanks,

Emanuel, can't you use the signal/slot system on drawn items, like curves? I thought you could make it so, when the mouse enters a particular curve it signals it; a user might just catch that signal. It would be the most inexpensive way since you don't have to test-point all the data.

Just wanted to say thanks for the code!

A note to people using Qt 5+, posF() has been replaced with localPos(). Other than that I dropped the code in and had it working instantly.

When the mouse is away from the curve, I get incorrect X and Y. Following condition will correct it.

if(plottable)
{
   ....  
}
else
   QToolTip::hideText();

Thanks Kwabena your code helps me alot.

Thanks a lot for this. But I have only question: The signal is:

void QCustomPlot::plottableClick ( QCPAbstractPlottable * plottable, QMouseEvent * event )	

so I had to modify a little your method in:
void plotMousePress(QCPAbstractPlottable * plot, QMouseEvent *event)
, even though the "plot" parameter is never used. If i want to use the method exactlly how you read here, i get some compilation error that said it's a problem between the Signal and Slot. Signal expects 2 parameters, but the slot has only one.

Thank you a lot,
Manich.

Thanks Kwabena for posting this code and DerManu for this great library.

I used this code as below. It worked well until I moved to QCustomPlot 2.0.0-beta. The version 2.0.0 is not supporting graph->data()->values() and QCPData in the foreach loop. I changed QCPData to QCPGraphData, but I still could not figure out how to access graph->data values. I don't want to go back and use Version 1.3.2. Could you suggest an alternative approach access graph data?

connect(ui->plotG, SIGNAL(mouseMove(QMouseEvent*)), this,SLOT(on_mouseOverPlotHeight(QMouseEvent*)));

void MainWindow::MouseOverPlotHeight(QMouseEvent *event)
{
    QCustomPlot *curPlot = ui->customPlot_Height;
    QString strNameX = "Days";
    QString strNameY = "Height";
    DisplayData(event, curPlot, strNameX, strNameY);
}

void MainWindow::DisplayData(QMouseEvent *event, QCustomPlot *curPlot, QString strNameX, QString strNameY)
{
    QCPAbstractPlottable *plottable = curPlot->plottableAt(event->localPos());
    if(plottable)
    {
        double x = curPlot->xAxis->pixelToCoord(event->localPos().x());
        double y = curPlot->yAxis->pixelToCoord(event->localPos().y());
        QCPGraph *graph =  qobject_cast<QCPGraph*>(plottable);if(graph)
        {
            double key = 0;
            double value = 0;
            bool ok = false;
            double m = std::numeric_limits<double>::max();
            foreach(QCPData data, graph->data()->values())
            {
                double d = sqrt((x - data.key)*(x - data.key) + (y - data.value)*(y - data.value));
                if(d < m)
                {
                    key = data.key;
                    value = data.value;
                    ok = true;
                    m = d;
                }
            }
            if(ok)
            {
                QToolTip::showText(event->globalPos(),
                tr("<table>"
                     "<tr>"
                       "<td>%L1:</td>" "<td>%L2</td>"
                     "</tr>"
                     "<tr>"
                       "<td>%L3:</td>" "<td>%L4</td>"
                     "</tr>"
                   "</table>").arg(strNameX).arg(key).arg(strNameY).arg(value),curPlot, curPlot->rect());
            }
        }
    }
    else
        QToolTip::hideText();
}

Could someone suggest me how change following foreach loop to access data values.

QCPGraph *graph =  qobject_cast<QCPGraph*>(plottable);
foreach(QCPData data, graph->data()->values())
{
       ........
}

I am using QCustomPlot 2.0.0-beta
Thanks

I came up with a solution that works with QCustomPlot 2.0 library. The credit should go to Kwabena for his original post. His code has been slightly modified and customized. I hope this will help other users who are looking for a working code.

Signal and SLOT:

connect(ui->customPlot_AgeHeight, SIGNAL(mouseMove(QMouseEvent*)), this,SLOT(on_mouseOverPlotHeight(QMouseEvent*)));

If you have multiple qcustomplots with different axis names, this intermediate function can be used to pass them. Here's the intermediate code:

void MainWindow::MouseOverPlotHeight(QMouseEvent *event)
{   
    QCustomPlot *curPlot = ui->customPlot_AgeHeight;
    QString strNameX = "Age";
    QString strNameY = "Height";
    DisplayCurveData(event, curPlot, strNameX, strNameY);
}
Modified Kwabena's code:
void MainWindow::DisplayCurveData(QMouseEvent *event, QCustomPlot *curPlot, QString strNameX, QString strNameY)
{
    QCPAbstractPlottable *plottable = curPlot->plottableAt(event->localPos());
    if(plottable)
    {
        double x = curPlot->xAxis->pixelToCoord(event->localPos().x());
        double y = curPlot->yAxis->pixelToCoord(event->localPos().y());

        QCPGraph *graph =  qobject_cast<QCPGraph*>(plottable);
        if (graph)
        {
            double key = 0;
            double value = 0;
            bool ok = false;
            double maxx = std::numeric_limits<double>::max();
            double maxy = std::numeric_limits<double>::max();

            QCPDataRange dataRange = graph->data()->dataRange();
            QCPGraphDataContainer::const_iterator begin = graph->data()->at(dataRange.begin()); 
            QCPGraphDataContainer::const_iterator end = graph->data()->at(dataRange.end()); 

            int n = end-begin;
            if (n>0)
            {
                double *dx = new double[n];
                double *dy = new double[n];

                int index =0;
                for (QCPGraphDataContainer::const_iterator it=begin; it<end; it++)
                {
                    dx[index] = qAbs(x - it->key);
                    dy[index] = qAbs(y - it->value);
                    if ((dx[index] < maxx) && (dy[index] < maxy))
                    {
                        key = it->key;
                        value = it->value;
                        ok = true;
                        maxx = dx[index];
                        maxy = dy[index];
                    }
                    index++;
                }
                delete dy;
                delete dx;

                if (ok)
                {
                    QToolTip::showText(event->globalPos(),
                    tr("<table>"
                         "<tr>"
                           "<td>%L1:</td>" "<td>%L2</td>"
                         "</tr>"
                         "<tr>"
                           "<td>%L3:</td>" "<td>%L4</td>"
                         "</tr>"
                      "</table>").arg(strNameX).arg(key).arg(strNameY).arg(value),curPlot, curPlot->rect());
                }
            }
        }
    }
    else
        QToolTip::hideText();
}

Thanks! This was exactly what I needed todo. You saved me a lot of time :)

Here is another tip to save someone elses time:

if ((dx[index] < maxx) && (dy[index] < maxy))

replace && with || if your value is not cumulative


Since QCustomPlot 2.0.0, all of this became much simpler:

First of all, you can use the data container's findBegin method, to easily get the data point close to a certain key (you can convert mouse cursor pixels to key axis coordinates via pixelToCoord of the key axis). It's also faster, since it uses binary search.

But actually you don't even have to do that. The most interaction methods now provide the closest data point to the interaction (click) event.
For example QCustomPot::plottableClick now has the parameter dataIndex, which provides you with the index of the closest data point.
If you want to determine the point at any time, you can use QCPGraph::selectTest, and convert the returned QVariant *details, into a QCPDataSelection (which will contain one point, which is the closest one to the specified coordinate). I just noticed that's not documented. What a shame, it will be documented in the next patch.

//EDIT: How to find the data point by using selectTest is explained in detail with example code at the bottom of the data selection mechanism page.

I tried jcr's code and this works perfect.
In my case I have a QCPItemLine as a cursor, which is connected with the MouseMove-Event that is in the position of the mouse.

How can I edit the code, that I get all the graph values in the position of the "cursor"?
(If there is no value, just show 0, 0 or something similar)

Below you can see how I did it. I think it's pretty simple solution:

Introduce a QCPItemTracer. The key point here is to set "setInterpolating(false)" This makes sure the item tracer is sticking only to valid data points and doesn't try to interpolate.

    // add the phase tracer (red circle) which sticks to the graph data:
    this->phaseTracer = new QCPItemTracer(this->customPlot);
    this->phaseTracer->setInterpolating(false);
    this->phaseTracer->setStyle(QCPItemTracer::tsCircle);
    this->phaseTracer->setPen(QPen(Qt::red));
    this->phaseTracer->setBrush(Qt::red);
    this->phaseTracer->setSize(10);

Connect mouseMove signal to a custom handling slot:

    connect(this->customPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(showPointValue(QMouseEvent*)));

Handle the event

void MainWindow::showPointValue( QMouseEvent* event )
{
    QCPGraph *graph = this->customPlot->graph(0);

    // Get selected graph (in my case selected means the plot is selected from the legend)
    for (int i=0; i<this->customPlot->graphCount(); ++i)
    {
        if( this->customPlot->legend->itemWithPlottable(this->customPlot->graph(i))->selected() )
        {
            graph = this->customPlot->graph(i);
            break;
        }
    }

    // Setup the item tracer
    this->phaseTracer->setGraph(graph);
    this->phaseTracer->setGraphKey(this->customPlot->xAxis->pixelToCoord(event->pos().x()));
    this->customPlot->replot();

    // **********Get the values from the item tracer's coords***********
    QPointF temp = this->phaseTracer->position->coords();

    // Show a tooltip which tells the values
    QToolTip::showText(event->globalPos(),
                        tr("<h4>%L1</h4>"
                        "<table>"
                            "<tr>"
                                "<td><h5>X: %L2</h5></td>" "<td>  ,  </td>" "<td><h5>Y: %L3</h5></td>"
                            "</tr>"
                        "</table>").arg(graph->name()).arg( QString::number( temp.x(), 'f', 0 ) ).arg( QString::number( temp.y(), 'f', 1 ) ), customPlot, customPlot->rect());
}

Hi there,

I assume my problem is VERY simple to solve but I'm somehow blocked to
see the obvious ...

I'm using QCustomPlot 2.0.0. I want to display a tooltip with some
information when the mouse curser is moved over a point in a scatter
plot. I know that I have to connect a slot method to the mouseMove()
signal and in this method I have to implement that functionality.
Some instructions on how to implement this I found in a post by DerManu in this thread.
So my code for the slot method looks as follows (the class PlotUI inherits
from QCustomPlot and represents my plot):

void PlotUI::createToolTip(QMouseEvent *event) {

    // 1. I determine the plottable under the cursor (if any). 
    QCPAbstractPlottable *plottable = plottableAt(event->pos());

    // 2. If it is a scatter graph, then I want to determine the data
    // point under the cursor position.
    QCPGraph *graph(dynamic_cast<QCPGraph*>(plottable));
    if (graph != nullptr) {
        int x = xAxis->pixelToCoord(event->pos().x());
        auto beginRange = graph->data()->findBegin(x);
        QToolTip::showText(event->globalPos(),
              tr("<table>"
                   "<tr>"
                   "<td>%L1:</td>" "<td>%L2</td>"
                   "</tr>"
                 "</table>").arg("Index").arg(beginRange->key), 
               this, rect());
    } else {
        QToolTip::hideText();
    }
}

To my big surprise the value of beginRange->key varies depending on
where exactly the cursor is pointing to in close proximity or even directly on
a data point of the graph. In my graph the data points are not very
close to each other so I think when moving the mouse cursor around a
specific data point only that data point's index should be returned.
So my assumption is that I need to do something more in order to
properly identify a specific data point. Any ideas?

I also tried out the other solution recommended by DerManu in his post
to this thread. This requires a mouse click on a data point. My
implementation looks as follows:

I have created a connect statement that connects the signal
QCustomPlot::plottableClick with a slot method
named displayToolTipOnClickedPoint(). This method looks as follows:

void PlotUI::displayToolTipOnClickedPoint(QCPAbstractPlottable *plottable,
                                          int dataIndex,
                                          QMouseEvent *event) {
    auto *graph(dynamic_cast<QCPGraph*>(plottable));
    if (graph) {
        auto *graphModel(static_cast<const ScatterGraph*>(model4Plottable(graph)));

        auto pathId(graphModel->determinePathId(dataIndex));
        QToolTip::showText(event->globalPos(),
                           tr("<table>"
                                "<tr>"
                                  "<td>%L1:</td>" "<td>%L2</td>"
                                "</tr>"
                              "</table>").arg("Path Id").arg(pathId), this, rect());
    } else {
        QToolTip::hideText();
    }
}

It works perfectly. No matter if directly clicking on the data point
or in close proximity of it the proper index is provided. But this
method requires a mouse click instead of a simple mouse over on the
data point. Any helpful hint to make that work would be highly appreciated.

Hi!

In my post above on October 31 I've linked the documentation to findBegin. This will explain why you're seeing what you're seeing. (findBegin and findEnd are used to find according data ranges which encompass the specified key.)

For your purpose, I'd recommend selectTest instead (also explained and linked above).

Your use case is also explained in detail with example code at the bottom of the data selection mechanism page.

Thanks a lot for your explanation! Inspected the code example in the data selection mechanism section of the documentation and got enlighted:-)

I just realized there's an error in the documentation:
selectTest returns a double but it's used directly as the if condition. It should rather be something like

double dist = selectTest(...);
if (dist >= 0 && dist < plot->selectionTolerance()) {...}

Of course, if you've already picked the plottable with plottableAt(), you won't need to check selectTest's return value at all and can directly use the details.

Also, I got an idea for a better plottableAt() method, which can actually directly return the data point index in an output parameter. Something like this will be added in the next minor version.