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"