File indexing completed on 2024-05-19 05:44:23

0001 /*
0002     SPDX-FileCopyrightText: 2015-2017 Milian Wolff <mail@milianw.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "chartwidget.h"
0008 
0009 #include <QApplication>
0010 #include <QCheckBox>
0011 #include <QFileDialog>
0012 #include <QLabel>
0013 #include <QMenu>
0014 #include <QPainter>
0015 #include <QPushButton>
0016 #include <QRubberBand>
0017 #include <QSpinBox>
0018 #include <QSvgGenerator>
0019 #include <QTextStream>
0020 #include <QToolBar>
0021 #include <QToolTip>
0022 #include <QVBoxLayout>
0023 
0024 #include <KChartChart>
0025 #include <KChartPlotter>
0026 
0027 #include <KChartBackgroundAttributes>
0028 #include <KChartCartesianCoordinatePlane>
0029 #include <KChartDataValueAttributes>
0030 #include <KChartFrameAttributes.h>
0031 #include <KChartGridAttributes>
0032 #include <KChartHeaderFooter>
0033 #include <KChartLegend>
0034 #include <KMessageBox>
0035 
0036 #include <KColorScheme>
0037 #include <KLocalizedString>
0038 
0039 #include "chartmodel.h"
0040 #include "chartproxy.h"
0041 #include "util.h"
0042 
0043 #include <cmath>
0044 #include <limits>
0045 
0046 using namespace KChart;
0047 
0048 namespace {
0049 KChart::TextAttributes fixupTextAttributes(KChart::TextAttributes attributes, const QPen& foreground, float pointSize)
0050 {
0051     attributes.setPen(foreground);
0052     auto fontSize = attributes.fontSize();
0053     fontSize.setAbsoluteValue(pointSize);
0054     attributes.setFontSize(fontSize);
0055     return attributes;
0056 }
0057 
0058 QPointF localPos(QMouseEvent* event)
0059 {
0060 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0061     return event->localPos();
0062 #else
0063     return event->position();
0064 #endif
0065 }
0066 
0067 QPoint globalPos(QMouseEvent* event)
0068 {
0069 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0070     return event->globalPos();
0071 #else
0072     return event->globalPosition().toPoint();
0073 #endif
0074 }
0075 
0076 class TimeAxis : public CartesianAxis
0077 {
0078     Q_OBJECT
0079 public:
0080     explicit TimeAxis(AbstractCartesianDiagram* diagram = nullptr)
0081         : CartesianAxis(diagram)
0082     {
0083     }
0084 
0085     const QString customizedLabel(const QString& label) const override
0086     {
0087         const auto time = label.toLongLong();
0088         if (m_summaryData.filterParameters.isFilteredByTime(m_summaryData.totalTime)) {
0089             return Util::formatTime(time) + QLatin1Char('\n')
0090                 + Util::formatTime(time - m_summaryData.filterParameters.minTime);
0091         }
0092         return Util::formatTime(time);
0093     }
0094 
0095     void setSummaryData(const SummaryData& summaryData)
0096     {
0097         m_summaryData = summaryData;
0098         update();
0099     }
0100 
0101 private:
0102     SummaryData m_summaryData;
0103 };
0104 
0105 class SizeAxis : public CartesianAxis
0106 {
0107     Q_OBJECT
0108 public:
0109     explicit SizeAxis(AbstractCartesianDiagram* diagram = nullptr)
0110         : CartesianAxis(diagram)
0111     {
0112     }
0113 
0114     const QString customizedLabel(const QString& label) const override
0115     {
0116         return Util::formatBytes(label.toLongLong());
0117     }
0118 };
0119 
0120 /// see also ProxyStyle which is responsible for unsetting SH_RubberBand_Mask
0121 class ChartRubberBand : public QRubberBand
0122 {
0123     Q_OBJECT
0124 public:
0125     explicit ChartRubberBand(QWidget* parent)
0126         : QRubberBand(QRubberBand::Rectangle, parent)
0127     {
0128     }
0129     ~ChartRubberBand() = default;
0130 
0131 protected:
0132     void paintEvent(QPaintEvent* event) override
0133     {
0134         auto brush = palette().highlight();
0135         if (brush != m_lastBrush) {
0136             auto color = brush.color();
0137             color.setAlpha(128);
0138             brush.setColor(color);
0139             m_cachedBrush = brush;
0140         } else {
0141             brush = m_cachedBrush;
0142         }
0143 
0144         QPainter painter(this);
0145         painter.fillRect(event->rect(), brush);
0146     }
0147 
0148 private:
0149     QBrush m_lastBrush;
0150     QBrush m_cachedBrush;
0151 };
0152 }
0153 
0154 ChartWidget::ChartWidget(QWidget* parent)
0155     : QWidget(parent)
0156     , m_chart(new Chart(this))
0157     , m_legend(new Legend(m_chart))
0158     , m_rubberBand(new ChartRubberBand(this))
0159 {
0160     auto m_chartToolBar = new QToolBar(this);
0161 
0162     auto m_exportAsButton = new QPushButton(i18n("Export As..."), this);
0163     connect(m_exportAsButton, &QPushButton::released, this, &ChartWidget::saveAs);
0164 
0165     auto m_showLegend = new QCheckBox(i18n("Show legend"), this);
0166     m_showLegend->setChecked(false);
0167     connect(m_showLegend, &QCheckBox::toggled, this, [=](bool show) {
0168         m_legend->setVisible(show);
0169         m_chart->update();
0170     });
0171 
0172     auto m_showTotal = new QCheckBox(i18n("Show total cost graph"), this);
0173     m_showTotal->setChecked(true);
0174     connect(m_showTotal, &QCheckBox::toggled, this, [=](bool show) {
0175         m_totalPlotter->setHidden(!show);
0176         m_chart->update();
0177     });
0178     m_legend->setVisible(m_showLegend->checkState());
0179 
0180     auto m_showDetailed = new QCheckBox(i18n("Show detailed cost graph"), this);
0181     m_showDetailed->setChecked(true);
0182     connect(m_showDetailed, &QCheckBox::toggled, this, [=](bool show) {
0183         m_detailedPlotter->setHidden(!show);
0184         m_chart->update();
0185     });
0186 
0187     auto stackedLabel = new QLabel(i18n("Stacked diagrams:"));
0188     m_stackedDiagrams = new QSpinBox(this);
0189     m_stackedDiagrams->setMinimum(0);
0190     m_stackedDiagrams->setMaximum(50);
0191     connect(m_stackedDiagrams, qOverload<int>(&QSpinBox::valueChanged), this,
0192             [=](int value) { m_model->setMaximumDatasetCount(value + 1); });
0193 
0194     m_chartToolBar->addWidget(m_exportAsButton);
0195     m_chartToolBar->addSeparator();
0196     m_chartToolBar->addWidget(m_showLegend);
0197     m_chartToolBar->addSeparator();
0198     m_chartToolBar->addWidget(m_showTotal);
0199     m_chartToolBar->addWidget(m_showDetailed);
0200     m_chartToolBar->addSeparator();
0201     m_chartToolBar->addWidget(stackedLabel);
0202     m_chartToolBar->addWidget(m_stackedDiagrams);
0203 
0204     auto layout = new QVBoxLayout(this);
0205     layout->setContentsMargins(0, 0, 0, 0);
0206     layout->setSpacing(0);
0207     layout->addWidget(m_chartToolBar);
0208     layout->addWidget(m_chart);
0209     setLayout(layout);
0210 
0211     auto* coordinatePlane = dynamic_cast<CartesianCoordinatePlane*>(m_chart->coordinatePlane());
0212     Q_ASSERT(coordinatePlane);
0213     coordinatePlane->setAutoAdjustGridToZoom(true);
0214     connect(coordinatePlane, &CartesianCoordinatePlane::needUpdate, this, &ChartWidget::updateRubberBand);
0215 
0216     m_chart->setCursor(Qt::IBeamCursor);
0217     m_chart->setMouseTracking(true);
0218     m_chart->installEventFilter(this);
0219 
0220     m_chart->setContextMenuPolicy(Qt::CustomContextMenu);
0221     connect(m_chart, &QWidget::customContextMenuRequested, this, [this](const QPoint& point) {
0222         if (!m_model)
0223             return;
0224 
0225         const auto isFiltered = m_summaryData.filterParameters.isFilteredByTime(m_summaryData.totalTime);
0226         if (!m_selection && !isFiltered)
0227             return;
0228 
0229         auto* menu = new QMenu(this);
0230         menu->setAttribute(Qt::WA_DeleteOnClose, true);
0231 
0232         if (m_selection) {
0233             auto* reparse = menu->addAction(QIcon::fromTheme(QStringLiteral("timeline-use-zone-on")),
0234                                             i18n("Filter In On Selection"));
0235             connect(reparse, &QAction::triggered, this, [this]() {
0236                 const auto startTime = std::min(m_selection.start, m_selection.end);
0237                 const auto endTime = std::max(m_selection.start, m_selection.end);
0238                 emit filterRequested(startTime, endTime);
0239             });
0240         }
0241 
0242         if (isFiltered) {
0243             auto* reset =
0244                 menu->addAction(QIcon::fromTheme(QStringLiteral("timeline-use-zone-off")), i18n("Reset Filter"));
0245             connect(reset, &QAction::triggered, this,
0246                     [this]() { emit filterRequested(0, std::numeric_limits<int64_t>::max()); });
0247         }
0248 
0249         menu->popup(m_chart->mapToGlobal(point));
0250     });
0251 }
0252 
0253 ChartWidget::~ChartWidget() = default;
0254 
0255 void ChartWidget::setSummaryData(const SummaryData& summaryData)
0256 {
0257     m_summaryData = summaryData;
0258     updateAxesTitle();
0259     if (m_bottomAxis) {
0260         static_cast<TimeAxis*>(m_bottomAxis)->setSummaryData(summaryData);
0261     }
0262 }
0263 
0264 void ChartWidget::setModel(ChartModel* model, bool minimalMode)
0265 {
0266     if (m_model == model)
0267         return;
0268     m_model = model;
0269 
0270     auto* coordinatePlane = dynamic_cast<CartesianCoordinatePlane*>(m_chart->coordinatePlane());
0271     Q_ASSERT(coordinatePlane);
0272     const auto diagrams = coordinatePlane->diagrams();
0273     for (auto diagram : diagrams) {
0274         coordinatePlane->takeDiagram(diagram);
0275         delete diagram;
0276     }
0277 
0278     if (minimalMode) {
0279         KChart::GridAttributes grid;
0280         grid.setSubGridVisible(false);
0281         coordinatePlane->setGlobalGridAttributes(grid);
0282     }
0283 
0284     KColorScheme scheme(QPalette::Active, KColorScheme::Window);
0285     QPen foreground(scheme.foreground().color());
0286 
0287     {
0288         KChart::GridAttributes grid = coordinatePlane->gridAttributes(Qt::Horizontal);
0289         // Do not align view on main grid line, stretch grid to match datasets
0290         grid.setAdjustBoundsToGrid(false, false);
0291         coordinatePlane->setGridAttributes(Qt::Horizontal, grid);
0292 
0293         m_legend->setOrientation(Qt::Vertical);
0294         m_legend->setTitleText(QString());
0295         m_legend->setSortOrder(Qt::DescendingOrder);
0296 
0297         RelativePosition relPos;
0298         relPos.setReferenceArea(coordinatePlane);
0299         relPos.setReferencePosition(Position::NorthWest);
0300         relPos.setAlignment(Qt::AlignTop | Qt::AlignLeft | Qt::AlignAbsolute);
0301         relPos.setHorizontalPadding(Measure(3.0, KChartEnums::MeasureCalculationModeAbsolute));
0302         relPos.setVerticalPadding(Measure(3.0, KChartEnums::MeasureCalculationModeAbsolute));
0303 
0304         m_legend->setFloatingPosition(relPos);
0305         m_legend->setTextAlignment(Qt::AlignLeft | Qt::AlignAbsolute);
0306 
0307         m_chart->addLegend(m_legend);
0308 
0309         BackgroundAttributes bkgAtt = m_legend->backgroundAttributes();
0310         QColor background = scheme.background(KColorScheme::AlternateBackground).color();
0311         background.setAlpha(200);
0312         bkgAtt.setBrush(QBrush(background));
0313         bkgAtt.setVisible(true);
0314 
0315         TextAttributes textAttr = fixupTextAttributes(m_legend->textAttributes(), foreground, font().pointSizeF() - 2);
0316         QFont legendFont(QStringLiteral("monospace"));
0317         legendFont.setStyleHint(QFont::TypeWriter);
0318         textAttr.setFont(legendFont);
0319 
0320         m_legend->setBackgroundAttributes(bkgAtt);
0321         m_legend->setTextAttributes(textAttr);
0322     }
0323 
0324     {
0325         m_totalPlotter = new Plotter(this);
0326         m_totalPlotter->setAntiAliasing(true);
0327         auto totalProxy = new ChartProxy(true, this);
0328         totalProxy->setSourceModel(model);
0329         m_totalPlotter->setModel(totalProxy);
0330         m_totalPlotter->setType(Plotter::Stacked);
0331 
0332         m_bottomAxis = new TimeAxis(m_totalPlotter);
0333         const auto axisTextAttributes =
0334             fixupTextAttributes(m_bottomAxis->textAttributes(), foreground, font().pointSizeF() - 2);
0335         m_bottomAxis->setTextAttributes(axisTextAttributes);
0336         const auto axisTitleTextAttributes = fixupTextAttributes(m_bottomAxis->titleTextAttributes(), foreground,
0337                                                                  font().pointSizeF() + (minimalMode ? (-2) : (+2)));
0338         m_bottomAxis->setTitleTextAttributes(axisTitleTextAttributes);
0339         m_bottomAxis->setPosition(CartesianAxis::Bottom);
0340         m_totalPlotter->addAxis(m_bottomAxis);
0341 
0342         m_rightAxis = model->type() == ChartModel::Allocations || model->type() == ChartModel::Temporary
0343             ? new CartesianAxis(m_totalPlotter)
0344             : new SizeAxis(m_totalPlotter);
0345         m_rightAxis->setTextAttributes(axisTextAttributes);
0346         m_rightAxis->setTitleTextAttributes(axisTitleTextAttributes);
0347         m_rightAxis->setPosition(CartesianAxis::Right);
0348         m_totalPlotter->addAxis(m_rightAxis);
0349 
0350         coordinatePlane->addDiagram(m_totalPlotter);
0351 
0352         m_legend->addDiagram(m_totalPlotter);
0353     }
0354 
0355     {
0356         m_detailedPlotter = new Plotter(this);
0357         m_detailedPlotter->setAntiAliasing(true);
0358         m_detailedPlotter->setType(Plotter::Stacked);
0359 
0360         auto proxy = new ChartProxy(false, this);
0361         proxy->setSourceModel(model);
0362         m_detailedPlotter->setModel(proxy);
0363         coordinatePlane->addDiagram(m_detailedPlotter);
0364 
0365         m_legend->addDiagram(m_detailedPlotter);
0366     }
0367 
0368     m_legend->hide();
0369 
0370     // If the dataset has 10 entries, one is for the total plot and the
0371     // remaining ones are for the detailed plot. We want to only change
0372     // the number of detailed plots, so we have to correct it.
0373     int maximumDatasetCount = m_model->maximumDatasetCount();
0374     m_stackedDiagrams->setValue(maximumDatasetCount - 1);
0375 
0376     updateToolTip();
0377     updateAxesTitle();
0378 }
0379 
0380 void ChartWidget::saveAs()
0381 {
0382     const auto saveFilename =
0383         QFileDialog::getSaveFileName(this, i18n("Save %1", windowTitle()), QString(),
0384                                      i18n("Raster Image (*.png *.jpg *.tiff);;Vector Image (*.svg)"));
0385 
0386     if (!saveFilename.isEmpty()) {
0387         if (QFileInfo(saveFilename).suffix() == QLatin1String("svg")) {
0388             // vector graphic format
0389             QSvgGenerator generator;
0390             generator.setFileName(saveFilename);
0391             generator.setSize(m_chart->size());
0392             generator.setViewBox(m_chart->rect());
0393 
0394             QPainter painter;
0395             painter.begin(&generator);
0396             m_chart->paint(&painter, m_chart->rect());
0397             painter.end();
0398         } else if (!m_chart->grab().save(saveFilename)) {
0399             // other format
0400             KMessageBox::error(this, i18n("Failed to save the image to %1", saveFilename));
0401         }
0402     }
0403 }
0404 
0405 void ChartWidget::updateToolTip()
0406 {
0407     if (!m_model)
0408         return;
0409 
0410     const auto startTime = std::min(m_selection.start, m_selection.end);
0411     const auto endTime = std::max(m_selection.start, m_selection.end);
0412 
0413     const auto startCost = m_model->totalCostAt(startTime);
0414     const auto endCost = m_model->totalCostAt(endTime);
0415 
0416     QString toolTip;
0417     if (!qFuzzyCompare(startTime, endTime)) {
0418         QTextStream stream(&toolTip);
0419         stream << "<qt><table cellpadding=2>";
0420         stream << i18n("<tr><th></th><th>Start</th><th>End</th><th>Delta</th></tr>");
0421         stream << i18n("<tr><th>Time</th><td>%1</td><td>%2</td><td>%3</td></tr>", Util::formatTime(startTime),
0422                        Util::formatTime(endTime), Util::formatTime(endTime - startTime));
0423         switch (m_model->type()) {
0424         case ChartModel::Consumed:
0425             stream << i18n("<tr><th>Consumed</th><td>%1</td><td>%2</td><td>%3</td></tr>", Util::formatBytes(startCost),
0426                            Util::formatBytes(endCost), Util::formatBytes(endCost - startCost));
0427             break;
0428         case ChartModel::Allocations:
0429             stream << i18n("<tr><th>Allocations</th><td>%1</td><td>%2</td><td>%3</td></tr>", startCost, endCost,
0430                            (endCost - startCost));
0431             break;
0432         case ChartModel::Temporary:
0433             stream << i18n("<tr><th>Temporary Allocations</th><td>%1</td><td>%2</td><td>%3</td></tr>", startCost,
0434                            endCost, (endCost - startCost));
0435             break;
0436         }
0437         stream << "</table></qt>";
0438     } else {
0439         switch (m_model->type()) {
0440         case ChartModel::Consumed:
0441             toolTip = i18n("<qt>Shows the heap memory consumption over time.<br>Click and drag to select a time range "
0442                            "for filtering.</qt>");
0443             break;
0444         case ChartModel::Allocations:
0445             toolTip = i18n("<qt>Shows number of memory allocations over time.<br>Click and drag to select a time range "
0446                            "for filtering.</qt>");
0447             break;
0448         case ChartModel::Temporary:
0449             toolTip = i18n("<qt>Shows number of temporary memory allocations over time. "
0450                            "A temporary allocation is one that is followed immediately by its "
0451                            "corresponding deallocation, without other allocations happening "
0452                            "in-between.<br>Click and drag to select a time range for filtering.</qt>");
0453             break;
0454         }
0455     }
0456 
0457     setToolTip(toolTip);
0458 }
0459 
0460 void ChartWidget::updateAxesTitle()
0461 {
0462     if (!m_model)
0463         return;
0464 
0465     // m_bottomAxis is always time, so we can just write it here instead of in headerData().
0466     m_bottomAxis->setTitleText(i18n("Elapsed Time"));
0467     m_rightAxis->setTitleText(m_model->typeString());
0468 
0469     if (m_summaryData.filterParameters.isFilteredByTime(m_summaryData.totalTime)) {
0470         m_bottomAxis->setTitleText(
0471             i18n("%1 (filtered from %2 to %3, Δ%4)", m_bottomAxis->titleText(),
0472                  Util::formatTime(m_summaryData.filterParameters.minTime),
0473                  Util::formatTime(m_summaryData.filterParameters.maxTime),
0474                  Util::formatTime(m_summaryData.filterParameters.maxTime - m_summaryData.filterParameters.minTime)));
0475         m_rightAxis->setTitleText(i18n("%1 (filtered delta)", m_rightAxis->titleText()));
0476     }
0477 }
0478 
0479 QSize ChartWidget::sizeHint() const
0480 {
0481     return {400, 50};
0482 }
0483 
0484 void ChartWidget::setSelection(const Range& selection)
0485 {
0486     if (selection == m_selection || !m_model)
0487         return;
0488 
0489     m_selection = selection;
0490 
0491     updateToolTip();
0492     updateRubberBand();
0493 
0494     emit selectionChanged(m_selection);
0495 }
0496 
0497 void ChartWidget::updateRubberBand()
0498 {
0499     if (!m_selection || !m_model) {
0500         m_rubberBand->hide();
0501         return;
0502     }
0503 
0504     auto* coordinatePlane = static_cast<CartesianCoordinatePlane*>(m_chart->coordinatePlane());
0505     const auto delta = m_chart->pos().x();
0506     const auto pixelStart = coordinatePlane->translate({m_selection.start, 0}).x() + delta;
0507     const auto pixelEnd = coordinatePlane->translate({m_selection.end, 0}).x() + delta;
0508     auto selectionRect = QRect(QPoint(pixelStart, 0), QPoint(pixelEnd, height() - 1));
0509     m_rubberBand->setGeometry(selectionRect.normalized());
0510     m_rubberBand->show();
0511 }
0512 
0513 bool ChartWidget::eventFilter(QObject* watched, QEvent* event)
0514 {
0515     Q_ASSERT(watched == m_chart);
0516 
0517     if (!m_model)
0518         return false;
0519 
0520     auto mapPosToTime = [this](const QPointF& pos) {
0521         auto* coordinatePlane = static_cast<CartesianCoordinatePlane*>(m_chart->coordinatePlane());
0522         return coordinatePlane->translateBack(pos).x();
0523     };
0524 
0525     if (auto* mouseEvent = dynamic_cast<QMouseEvent*>(event)) {
0526         if (mouseEvent->button() == Qt::LeftButton || mouseEvent->buttons() == Qt::LeftButton) {
0527             const auto time = mapPosToTime(localPos(mouseEvent));
0528 
0529             auto selection = m_selection;
0530             selection.end = time;
0531             if (event->type() == QEvent::MouseButtonPress) {
0532                 selection.start = time;
0533                 m_chart->setCursor(Qt::SizeHorCursor);
0534                 m_cachedChart = m_chart->grab();
0535             } else if (event->type() == QEvent::MouseButtonRelease) {
0536                 m_chart->setCursor(Qt::IBeamCursor);
0537                 m_cachedChart = {};
0538             }
0539 
0540             setSelection(selection);
0541             QToolTip::showText(globalPos(mouseEvent), toolTip(), this);
0542             return true;
0543         } else if (event->type() == QEvent::MouseMove && !mouseEvent->buttons()) {
0544             updateStatusTip(mapPosToTime(localPos(mouseEvent)));
0545         }
0546     } else if (event->type() == QEvent::Paint && !m_cachedChart.isNull()) {
0547         // use the cached chart while interacting with the rubber band
0548         // otherwise, use the normal paint even as that one is required for
0549         // the mouse mapping etc. to work correctly...
0550         QPainter painter(m_chart);
0551         painter.drawPixmap(m_chart->rect(), m_cachedChart);
0552         return true;
0553     }
0554     return false;
0555 }
0556 
0557 void ChartWidget::updateStatusTip(qint64 time)
0558 {
0559     if (!m_model)
0560         return;
0561 
0562     const auto text = [=]() {
0563         if (time < 0 || time > m_summaryData.filterParameters.maxTime) {
0564             return i18n("Click and drag to select time range for filtering.");
0565         }
0566 
0567         const auto cost = m_model->totalCostAt(time);
0568         switch (m_model->type()) {
0569         case ChartModel::Consumed:
0570             return i18n("T = %1, Consumed: %2. Click and drag to select time range for filtering.",
0571                         Util::formatTime(time), Util::formatBytes(cost));
0572             break;
0573         case ChartModel::Allocations:
0574             return i18n("T = %1, Allocations: %2. Click and drag to select time range for filtering.",
0575                         Util::formatTime(time), cost);
0576             break;
0577         case ChartModel::Temporary:
0578             return i18n("T = %1, Temporary Allocations: %2. Click and drag to select time range for filtering.",
0579                         Util::formatTime(time), cost);
0580             break;
0581         }
0582         Q_UNREACHABLE();
0583     }();
0584     setStatusTip(text);
0585 
0586     // force update
0587     QStatusTipEvent event(text);
0588     QApplication::sendEvent(this, &event);
0589 }
0590 
0591 #include "chartwidget.moc"
0592 
0593 #include "moc_chartwidget.cpp"