tooltips implemented for charts (if using QWT) - except histogram
authorAlexey Chernobaev <achernobaev@dev.rtsoft.ru>
Thu, 15 Mar 2018 18:14:23 +0000 (21:14 +0300)
committerAlexey Chernobaev <achernobaev@dev.rtsoft.ru>
Thu, 15 Mar 2018 18:14:23 +0000 (21:14 +0300)
src/analyze/gui/chartmodel.cpp
src/analyze/gui/chartmodel.h
src/analyze/gui/chartmodel2qwtseriesdata.cpp
src/analyze/gui/chartwidgetqwtplot.cpp
src/analyze/gui/chartwidgetqwtplot.h
src/analyze/gui/util.cpp
src/analyze/gui/util.h

index 71fe6e6e6fd0b5176079c2edb1c7a0e648948972..faf49bb6c27f0794e8695e3c211117373d5a82bf 100644 (file)
@@ -31,6 +31,8 @@
 #include <KLocalizedString>
 #endif
 
+#include <algorithm>
+
 #include <QBrush>
 #include <QDebug>
 #include <QPen>
@@ -165,7 +167,7 @@ QVariant ChartModel::data(const QModelIndex& index, int role) const
                             byteCost(), time);
             }
         } else {
-            const auto label = m_data.labels.value(column).toHtmlEscaped();
+            const auto label = Util::wrapLabel(m_data.labels.value(column), 96);
             switch (m_type) {
             case Allocations:
                 return i18n("<qt>%2 allocations after %3 from:<p "
@@ -214,9 +216,16 @@ void ChartModel::resetData(const ChartData& data)
     Q_ASSERT(m_data.labels.size() < ChartRows::MAX_NUM_COST);
     beginResetModel();
     m_data = data;
+    m_timestamps.clear();
+    const int rows = rowCount();
+    m_timestamps.reserve(rows);
+    for (int row = 0; row < rows; ++row)
+    {
+        m_timestamps.append(m_data.rows[row].timeStamp);
+    }
     m_columnDataSetBrushes.clear();
     m_columnDataSetPens.clear();
-    const auto columns = columnCount();
+    const int columns = columnCount();
     for (int i = 0; i < columns; ++i) {
         auto color = colorForColumn(i, columns);
 #ifdef QWT_FOUND
@@ -232,6 +241,7 @@ void ChartModel::clearData()
 {
     beginResetModel();
     m_data = {};
+    m_timestamps = {};
     m_columnDataSetBrushes = {};
     m_columnDataSetPens = {};
     endResetModel();
@@ -239,7 +249,12 @@ void ChartModel::clearData()
 
 qint64 ChartModel::getTimestamp(int row) const
 {
-    return m_data.rows[row].timeStamp;
+    return m_timestamps[row];
+}
+
+qint64 ChartModel::getCost(int row, int column) const
+{
+    return m_data.rows[row].cost[column / 2];
 }
 
 QString ChartModel::getColumnLabel(int column) const
@@ -256,3 +271,25 @@ const QBrush& ChartModel::getColumnDataSetBrush(int column) const
 {
     return m_columnDataSetBrushes[column];
 }
+
+int ChartModel::getRowForTimestamp(qint64 timestamp) const
+{
+    // check if 'timestamp' is not greater than the maximum available timestamp
+    int lastIndex = m_timestamps.size() - 1;
+    if (!((lastIndex >= 0) && (timestamp <= m_timestamps[lastIndex])))
+    {
+        return false;
+    }
+    int result;
+    // find the first element that is greater than 'timestamp'
+    auto up_it = std::upper_bound(m_timestamps.begin(), m_timestamps.end(), timestamp);
+    if (up_it != m_timestamps.end())
+    {
+        result = up_it - m_timestamps.begin() - 1;
+    }
+    else // all elements are not greater than 'timestamp'
+    {
+        result = lastIndex; // due to check in the beginning of the function
+    }
+    return result;
+}
index 6c925348227d2a8df2187c1030c5e43c8bb721fb..6abb3c8e9df0c019efeb9f0641c90e0db2e508bb 100644 (file)
@@ -72,10 +72,18 @@ public:
     int columnCount(const QModelIndex& parent = QModelIndex()) const override;
 
     qint64 getTimestamp(int row) const;
+    qint64 getCost(int row, int column) const;
     QString getColumnLabel(int column) const;
     const QPen& getColumnDataSetPen(int column) const;
     const QBrush& getColumnDataSetBrush(int column) const;
 
+    // get an index of the chart row which timestamp is less than or equal to 'timestamp'
+    // and also it's the maximum from all such timestamps (i.e. the row's timestamp is the
+    // nearest to 'timestamp' to the left or is equal to 'timestamp') - if 'timestamp' is
+    // not less than the minimum timestamp and not greater than the maximum timestamp
+    // of all rows, otherwise return -1
+    int getRowForTimestamp(qint64 timestamp) const;
+
 public slots:
     void resetData(const ChartData& data);
     void clearData();
@@ -83,6 +91,7 @@ public slots:
 private:
     ChartData m_data;
     Type m_type;
+    QVector<qint64> m_timestamps;
     // we cache the pens and brushes as constructing them requires allocations
     // otherwise
     QVector<QPen> m_columnDataSetPens;
index cf96d6c51d91e9f6980be398e119dc90f2baee5d..24f44353812df622dd3dc44c175aaf8cc1453096 100644 (file)
@@ -11,13 +11,13 @@ void ChartModel2QwtSeriesData::updateBoundingRect()
     int rows = m_model->rowCount();
     if (rows > 0)
     {
-        m_boundingRect.setLeft(m_model->data(m_model->index(0, 0)).toDouble());
-        m_boundingRect.setTop(m_model->data(m_model->index(0, m_column)).toDouble());
-        m_boundingRect.setRight(m_model->data(m_model->index(rows - 1, 0)).toDouble());
-        qreal maxCost = -1E9;
+        m_boundingRect.setLeft(m_model->getTimestamp(0));
+        m_boundingRect.setTop(m_model->getCost(0, m_column));
+        m_boundingRect.setRight(m_model->getTimestamp(rows - 1));
+        qint64 maxCost = -1;
         for (int row = 0; row < rows; ++row)
         {
-            qreal cost = m_model->data(m_model->index(row, m_column)).toDouble();
+            qint64 cost = m_model->getCost(row, m_column);
             if (cost > maxCost)
             {
                 maxCost = cost;
@@ -25,14 +25,16 @@ void ChartModel2QwtSeriesData::updateBoundingRect()
         }
         m_boundingRect.setBottom(maxCost);
     }
+    else
+    {
+        m_boundingRect = {};
+    }
 }
 
 QPointF ChartModel2QwtSeriesData::sample(size_t i) const
 {
     int row = (int)i;
-    qreal timeStamp = m_model->data(m_model->index(row, 0)).toDouble();
-    qreal cost = m_model->data(m_model->index(row, m_column)).toDouble();
-    return QPointF(timeStamp, cost);
+    return QPointF(m_model->getTimestamp(row), m_model->getCost(row, m_column));
 }
 
 QRectF ChartModel2QwtSeriesData::boundingRect() const
index badf0c71bb84a2505a885b2acdbf3d4e90486f2c..f081a68a0a0ef93dbfda6e2ca029190823f4f348 100644 (file)
@@ -12,7 +12,9 @@
 #include <qwt_scale_draw.h>
 #include <qwt_symbol.h>
 
-#include <QRegularExpression>
+#include <QToolTip>
+
+#include <limits>
 
 class TimeScaleDraw: public QwtScaleDraw
 {
@@ -44,16 +46,31 @@ public:
 protected:
     virtual QwtText trackerTextF(const QPointF &pos) const
     {
-        QString value;
-        if (m_plot->isSizeModel())
+//qDebug() << "pos.x=" << pos.x() << "; pos.y=" << pos.y();
+        if ((pos.x() < 0) || (pos.y() < 0))
+        {
+            return {};
+        }
+        QString s;
+        if (m_plot->getCurveTooltip(pos, s))
         {
-            value = Util::formatByteSize(pos.y());
+            m_plot->clearTooltip();
         }
-        else
+        else // show default text
         {
-            value = QString::number((qint64)pos.y());
+            QString value;
+            if (m_plot->isSizeModel())
+            {
+                value = Util::formatByteSize(pos.y());
+            }
+            else
+            {
+                value = QString::number((qint64)pos.y());
+            }
+            s = QString(" %1 : %2 ").arg(Util::formatTime((qint64)pos.x())).arg(value);
+            m_plot->restoreTooltip();
         }
-        QwtText text(QString(" %1 : %2 ").arg(Util::formatTime((qint64)pos.x())).arg(value));
+        QwtText text(s);
         text.setColor(Qt::white);
         QColor c = rubberBandPen().color();
         text.setBorderPen(QPen(c));
@@ -115,70 +132,10 @@ void ChartWidgetQwtPlot::setOptions(Options options)
     }
 }
 
-static QString getCurveTitle(QString label)
-{
-    const int MaxLineLength = 48;
-    const int LastLineExtra = 12; // take into account the label continuation " (max=...)"
-
-    int labelLength = label.size();
-    if (labelLength + LastLineExtra <= MaxLineLength)
-    {
-        return label.toHtmlEscaped();
-    }
-    static QRegularExpression delimBefore("[(<]");
-    static QRegularExpression delimAfter("[- .,)>\\/]");
-    QString result;
-    do
-    {
-        int i = -1;
-        int wrapAfter = 0;
-        int i1 = label.indexOf(delimBefore, MaxLineLength);
-        int i2 = label.indexOf(delimAfter, MaxLineLength - 1);
-        if (i1 >= 0)
-        {
-            if (i2 >= 0)
-            {
-                if (i2 < i1)
-                {
-                    i = i2;
-                    wrapAfter = 1;
-                }
-                else
-                {
-                    i = i1;
-                }
-            }
-            else
-            {
-                i = i1;
-            }
-        }
-        else
-        {
-            i = i2;
-            wrapAfter = 1;
-        }
-        if (i < 0)
-        {
-            break;
-        }
-        i += wrapAfter;
-        result += label.left(i).toHtmlEscaped();
-        label.remove(0, i);
-        if (label.isEmpty()) // special: avoid <br> at the end
-        {
-            return result;
-        }
-        result += "<br>";
-        labelLength -= i;
-    }
-    while (labelLength + LastLineExtra > MaxLineLength);
-    result += label.toHtmlEscaped();
-    return result;
-}
-
 void ChartWidgetQwtPlot::rebuild(bool resetZoomAndPan)
 {
+    const int MaxTitleLineLength = 48;
+
     detachItems();
 
     if (resetZoomAndPan)
@@ -227,10 +184,12 @@ void ChartWidgetQwtPlot::rebuild(bool resetZoomAndPan)
         QRectF bounds = adapter->boundingRect();
         qint64 maxCost = bounds.bottom();
 
-        QString title = getCurveTitle(m_model->getColumnLabel(column));
-        title += QString(" (max=<b>%1</b>)").arg(
-            m_isSizeModel ? Util::formatByteSize(maxCost) : QString::number(maxCost));
-        auto curve = new QwtPlotCurve(title);
+        QString titleEnd = QString(" (max=<b>%1</b>)").arg(m_isSizeModel
+            ? Util::formatByteSize(maxCost) : QString::number(maxCost));
+        int lastLineExtra = titleEnd.length() - 2 * 3; // minus the two "<b>" tags length
+        auto curve = new QwtPlotCurve(
+            Util::wrapLabel(m_model->getColumnLabel(column), MaxTitleLineLength, lastLineExtra) +
+            titleEnd);
         curve->setRenderHint(QwtPlotItem::RenderAntialiased, true);
         curve->setYAxis(QwtPlot::yRight);
 
@@ -295,3 +254,64 @@ void ChartWidgetQwtPlot::resetZoom()
         replot();
     }
 }
+
+bool ChartWidgetQwtPlot::getCurveTooltip(const QPointF &position, QString &tooltip) const
+{
+    qint64 timestamp = position.x();
+    qreal cost = position.y();
+    if ((timestamp < 0) || (cost < 0))
+    {
+        return false;
+    }
+    int row = m_model->getRowForTimestamp(position.x());
+    if (row < 0)
+    {
+        return false;
+    }
+    // find a column which value (cost) for the row found is greater than or equal to
+    // 'cost' and at the same time this value is the smallest from all such values
+    qint64 minCostFound = std::numeric_limits<qint64>::max();
+    int columnFound = -1;
+    int column = 1;
+    if (!hasOption(ShowTotal))
+    {
+        column += 2;
+    }
+    int columns = m_model->columnCount();
+    for (; column < columns; column += 2)
+    {
+        qint64 columnCost = m_model->getCost(row, column);
+        if ((columnCost >= cost) && (columnCost <= minCostFound))
+        {
+            minCostFound = columnCost;
+            columnFound = column;
+        }
+    }
+    if (columnFound >= 0)
+    {
+//qDebug() << "timestamp: " << timestamp << " ms; row timestamp: " << m_model->getTimestamp(row) << " ms; cost="
+//         << cost << "; row=" << row << "; minCostFound=" << minCostFound;
+        tooltip = QString(" %1 ").arg(
+            m_model->data(m_model->index(row, columnFound), Qt::ToolTipRole).toString());
+        return true;
+    }
+    return false;
+}
+
+void ChartWidgetQwtPlot::clearTooltip()
+{
+    if (m_plotTooltip.isEmpty())
+    {
+        m_plotTooltip = parentWidget()->toolTip();
+    }
+    parentWidget()->setToolTip(QString());
+    QToolTip::hideText();
+}
+
+void ChartWidgetQwtPlot::restoreTooltip()
+{
+    if (!m_plotTooltip.isEmpty())
+    {
+        parentWidget()->setToolTip(m_plotTooltip);
+    }
+}
index 95246394bd55d813f14b12068cfe5af37d1eacc2..1eb0cc22024fd4dd7ccc9aeebda11fad5a68657c 100644 (file)
@@ -25,6 +25,8 @@ public:
 
     void setModel(ChartModel* model);
 
+    bool isSizeModel() const { return m_isSizeModel; }
+
     Options options() const { return m_options; }
 
     bool hasOption(Options option) const { return (m_options & option) != 0; }
@@ -37,9 +39,15 @@ public:
 
     void resetZoom();
 
-    bool isSizeModel() const { return m_isSizeModel; }
+    bool getCurveTooltip(const QPointF &position, QString &tooltip) const;
 
 private:
+    friend class Zoomer;
+
+    void clearTooltip();
+
+    void restoreTooltip();
+
     ChartModel *m_model;
 
     bool m_isSizeModel;
@@ -51,6 +59,8 @@ private:
     Zoomer *m_zoomer;
 
     QwtScaleDiv m_xScaleDiv, m_yScaleDiv;
+
+    QString m_plotTooltip;
 };
 
 inline ChartWidgetQwtPlot::Options operator | (ChartWidgetQwtPlot::Options i, ChartWidgetQwtPlot::Options f)
index 3a29d8b9f8df085533d036b9854f328f0144f07d..00ea260890eeedb4f289dde09a376ecf61d0521a 100644 (file)
@@ -18,6 +18,7 @@
 
 #include "util.h"
 
+#include <QRegularExpression>
 #include <QString>
 
 #ifndef NO_K_LIB
@@ -85,3 +86,63 @@ QString Util::formatByteSize(double size, int precision)
     return result;
 #endif
 }
+
+QString Util::wrapLabel(QString label, int maxLineLength, int lastLineExtra,
+    const QString& delimiter)
+{
+    int labelLength = label.size();
+    if (labelLength + lastLineExtra <= maxLineLength)
+    {
+        return label.toHtmlEscaped();
+    }
+    static QRegularExpression delimBefore("[(<]");
+    static QRegularExpression delimAfter("[- .,)>\\/]");
+    QString result;
+    do
+    {
+        int i = -1;
+        int wrapAfter = 0;
+        int i1 = label.indexOf(delimBefore, maxLineLength);
+        int i2 = label.indexOf(delimAfter, maxLineLength - 1);
+        if (i1 >= 0)
+        {
+            if (i2 >= 0)
+            {
+                if (i2 < i1)
+                {
+                    i = i2;
+                    wrapAfter = 1;
+                }
+                else
+                {
+                    i = i1;
+                }
+            }
+            else
+            {
+                i = i1;
+            }
+        }
+        else
+        {
+            i = i2;
+            wrapAfter = 1;
+        }
+        if (i < 0)
+        {
+            break;
+        }
+        i += wrapAfter;
+        result += label.left(i).toHtmlEscaped();
+        label.remove(0, i);
+        if (label.isEmpty()) // special: avoid <br> at the end
+        {
+            return result;
+        }
+        result += delimiter;
+        labelLength -= i;
+    }
+    while (labelLength + lastLineExtra > maxLineLength);
+    result += label.toHtmlEscaped();
+    return result;
+}
index 994a7feb1913c557ce7ff2f80ab7bad928e5ea4c..7f651a0b5b4cfb993c049bd6257f66082563212b 100644 (file)
@@ -20,8 +20,7 @@
 #define UTIL_H
 
 #include <qglobal.h>
-
-class QString;
+#include <QString>
 
 namespace Util {
 
@@ -29,6 +28,9 @@ QString formatTime(qint64 ms);
 
 QString formatByteSize(double size, int precision = 1);
 
+QString wrapLabel(QString label, int maxLineLength, int lastLineExtra = 0,
+                  const QString &delimiter = QString("<br>"));
+
 }
 
 #endif // UTIL_H