File indexing completed on 2025-01-26 03:34:05
0001 /* 0002 File : BoxPlot.cpp 0003 Project : LabPlot 0004 Description : Box Plot 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2021-2022 Alexander Semke <alexander.semke@web.de> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 #include "BoxPlot.h" 0012 #include "BoxPlotPrivate.h" 0013 #include "backend/core/AbstractColumn.h" 0014 #include "backend/core/Folder.h" 0015 #include "backend/core/Settings.h" 0016 #include "backend/core/column/Column.h" 0017 #include "backend/lib/XmlStreamReader.h" 0018 #include "backend/lib/commandtemplates.h" 0019 #include "backend/lib/trace.h" 0020 #include "backend/spreadsheet/Spreadsheet.h" 0021 #include "backend/worksheet/Background.h" 0022 #include "backend/worksheet/Line.h" 0023 #include "backend/worksheet/Worksheet.h" 0024 #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h" 0025 #include "backend/worksheet/plots/cartesian/CartesianPlot.h" 0026 #include "backend/worksheet/plots/cartesian/Symbol.h" 0027 #include "kdefrontend/GuiTools.h" 0028 #include "tools/ImageTools.h" 0029 0030 #include <QActionGroup> 0031 #include <QApplication> 0032 #include <QGraphicsSceneMouseEvent> 0033 #include <QMenu> 0034 #include <QPainter> 0035 0036 #include <KConfig> 0037 #include <KConfigGroup> 0038 #include <KLocalizedString> 0039 0040 /** 0041 * \class BoxPlot 0042 * \brief Box Plot 0043 */ 0044 0045 BoxPlot::BoxPlot(const QString& name) 0046 : Plot(name, new BoxPlotPrivate(this), AspectType::BoxPlot) { 0047 init(); 0048 } 0049 0050 BoxPlot::BoxPlot(const QString& name, BoxPlotPrivate* dd) 0051 : Plot(name, dd, AspectType::BoxPlot) { 0052 init(); 0053 } 0054 0055 // no need to delete the d-pointer here - it inherits from QGraphicsItem 0056 // and is deleted during the cleanup in QGraphicsScene 0057 BoxPlot::~BoxPlot() = default; 0058 0059 void BoxPlot::init() { 0060 Q_D(BoxPlot); 0061 0062 KConfig config; 0063 KConfigGroup group = config.group(QStringLiteral("BoxPlot")); 0064 0065 // general 0066 d->ordering = (BoxPlot::Ordering)group.readEntry(QStringLiteral("Ordering"), (int)BoxPlot::Ordering::None); 0067 d->whiskersType = (BoxPlot::WhiskersType)group.readEntry(QStringLiteral("WhiskersType"), (int)BoxPlot::WhiskersType::IQR); 0068 d->whiskersRangeParameter = group.readEntry(QStringLiteral("WhiskersIQRParameter"), 1.5); 0069 d->orientation = (BoxPlot::Orientation)group.readEntry(QStringLiteral("Orientation"), (int)BoxPlot::Orientation::Vertical); 0070 d->variableWidth = group.readEntry(QStringLiteral("VariableWidth"), false); 0071 d->widthFactor = group.readEntry(QStringLiteral("WidthFactor"), 1.0); 0072 d->notchesEnabled = group.readEntry(QStringLiteral("NotchesEnabled"), false); 0073 0074 // box 0075 d->addBackground(group); 0076 d->addBorderLine(group); 0077 d->addMedianLine(group); 0078 0079 // markers 0080 d->symbolMean = new Symbol(QStringLiteral("symbolMean")); 0081 addChild(d->symbolMean); 0082 d->symbolMean->setHidden(true); 0083 d->symbolMean->init(group); 0084 d->symbolMean->setStyle(Symbol::Style::Square); 0085 connect(d->symbolMean, &Symbol::updateRequested, [=] { 0086 d->recalcShapeAndBoundingRect(); 0087 }); 0088 connect(d->symbolMean, &Symbol::updatePixmapRequested, [=] { 0089 d->updatePixmap(); 0090 }); 0091 0092 d->symbolMedian = new Symbol(QStringLiteral("symbolMedian")); 0093 addChild(d->symbolMedian); 0094 d->symbolMedian->setHidden(true); 0095 d->symbolMedian->init(group); 0096 d->symbolMedian->setStyle(Symbol::Style::NoSymbols); 0097 connect(d->symbolMedian, &Symbol::updateRequested, [=] { 0098 d->recalcShapeAndBoundingRect(); 0099 }); 0100 connect(d->symbolMedian, &Symbol::updatePixmapRequested, [=] { 0101 d->updatePixmap(); 0102 }); 0103 0104 d->symbolOutlier = new Symbol(QStringLiteral("symbolOutlier")); 0105 addChild(d->symbolOutlier); 0106 d->symbolOutlier->setHidden(true); 0107 d->symbolOutlier->init(group); 0108 connect(d->symbolOutlier, &Symbol::updateRequested, [=] { 0109 d->recalcShapeAndBoundingRect(); 0110 }); 0111 connect(d->symbolOutlier, &Symbol::updatePixmapRequested, [=] { 0112 d->updatePixmap(); 0113 }); 0114 0115 d->symbolFarOut = new Symbol(QStringLiteral("symbolFarOut")); 0116 addChild(d->symbolFarOut); 0117 d->symbolFarOut->setHidden(true); 0118 d->symbolFarOut->init(group); 0119 d->symbolFarOut->setStyle(Symbol::Style::Plus); 0120 connect(d->symbolFarOut, &Symbol::updateRequested, [=] { 0121 d->recalcShapeAndBoundingRect(); 0122 }); 0123 connect(d->symbolFarOut, &Symbol::updatePixmapRequested, [=] { 0124 d->updatePixmap(); 0125 }); 0126 0127 d->symbolData = new Symbol(QStringLiteral("symbolData")); 0128 addChild(d->symbolData); 0129 d->symbolData->setHidden(true); 0130 d->symbolData->init(group); 0131 d->symbolData->setStyle(Symbol::Style::NoSymbols); 0132 d->symbolData->setOpacity(0.5); 0133 connect(d->symbolData, &Symbol::updateRequested, [=] { 0134 d->recalcShapeAndBoundingRect(); 0135 }); 0136 connect(d->symbolData, &Symbol::updatePixmapRequested, [=] { 0137 d->updatePixmap(); 0138 }); 0139 0140 d->symbolWhiskerEnd = new Symbol(QStringLiteral("symbolWhiskerEnd")); 0141 addChild(d->symbolWhiskerEnd); 0142 d->symbolWhiskerEnd->setHidden(true); 0143 d->symbolWhiskerEnd->init(group); 0144 d->symbolWhiskerEnd->setStyle(Symbol::Style::NoSymbols); 0145 connect(d->symbolWhiskerEnd, &Symbol::updateRequested, [=] { 0146 d->recalcShapeAndBoundingRect(); 0147 }); 0148 connect(d->symbolWhiskerEnd, &Symbol::updatePixmapRequested, [=] { 0149 d->updatePixmap(); 0150 }); 0151 0152 d->jitteringEnabled = group.readEntry(QStringLiteral("JitteringEnabled"), true); 0153 0154 // whiskers 0155 d->whiskersLine = new Line(QString()); 0156 d->whiskersLine->setPrefix(QStringLiteral("Whiskers")); 0157 d->whiskersLine->setCreateXmlElement(false); // whiskers element is created in BoxPlot::save() 0158 d->whiskersLine->setHidden(true); 0159 addChild(d->whiskersLine); 0160 d->whiskersLine->init(group); 0161 connect(d->whiskersLine, &Line::updatePixmapRequested, [=] { 0162 d->updatePixmap(); 0163 }); 0164 connect(d->whiskersLine, &Line::updateRequested, [=] { 0165 d->recalcShapeAndBoundingRect(); 0166 }); 0167 0168 d->whiskersCapSize = group.readEntry(QStringLiteral("WhiskersCapSize"), Worksheet::convertToSceneUnits(5.0, Worksheet::Unit::Point)); 0169 d->whiskersCapLine = new Line(QString()); 0170 d->whiskersCapLine->setPrefix(QStringLiteral("WhiskersCap")); 0171 d->whiskersCapLine->setCreateXmlElement(false); // whiskers cap element is created in BoxPlot::save() 0172 d->whiskersCapLine->setHidden(true); 0173 addChild(d->whiskersCapLine); 0174 d->whiskersCapLine->init(group); 0175 connect(d->whiskersCapLine, &Line::updatePixmapRequested, [=] { 0176 d->updatePixmap(); 0177 }); 0178 connect(d->whiskersCapLine, &Line::updateRequested, [=] { 0179 d->recalcShapeAndBoundingRect(); 0180 }); 0181 0182 // marginal plots (rug, BoxPlot, boxplot) 0183 d->rugEnabled = group.readEntry(QStringLiteral("RugEnabled"), false); 0184 d->rugLength = group.readEntry(QStringLiteral("RugLength"), Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point)); 0185 d->rugWidth = group.readEntry(QStringLiteral("RugWidth"), 0.0); 0186 d->rugOffset = group.readEntry(QStringLiteral("RugOffset"), 0.0); 0187 } 0188 0189 /*! 0190 Returns an icon to be used in the project explorer. 0191 */ 0192 QIcon BoxPlot::icon() const { 0193 return BoxPlot::staticIcon(); 0194 } 0195 0196 QIcon BoxPlot::staticIcon() { 0197 QPainter pa; 0198 pa.setRenderHint(QPainter::Antialiasing); 0199 int iconSize = 20; 0200 QPixmap pm(iconSize, iconSize); 0201 0202 QPen pen(Qt::SolidLine); 0203 pen.setColor(GuiTools::isDarkMode() ? Qt::white : Qt::black); 0204 pen.setWidthF(0.0); 0205 0206 pm.fill(Qt::transparent); 0207 pa.begin(&pm); 0208 pa.setPen(pen); 0209 pa.drawRect(6, 6, 8, 8); // box 0210 pa.drawLine(10, 6, 10, 0); // upper whisker 0211 pa.drawLine(10, 14, 10, 20); // lower whisker 0212 pa.end(); 0213 0214 return {pm}; 0215 } 0216 0217 void BoxPlot::initActions() { 0218 // Orientation 0219 auto* orientationActionGroup = new QActionGroup(this); 0220 orientationActionGroup->setExclusive(true); 0221 connect(orientationActionGroup, &QActionGroup::triggered, this, &BoxPlot::orientationChangedSlot); 0222 0223 orientationHorizontalAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-move-horizontal")), i18n("Horizontal"), orientationActionGroup); 0224 orientationHorizontalAction->setCheckable(true); 0225 0226 orientationVerticalAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-move-vertical")), i18n("Vertical"), orientationActionGroup); 0227 orientationVerticalAction->setCheckable(true); 0228 } 0229 0230 void BoxPlot::initMenus() { 0231 this->initActions(); 0232 0233 // Orientation 0234 orientationMenu = new QMenu(i18n("Orientation")); 0235 orientationMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-cross"))); 0236 orientationMenu->addAction(orientationHorizontalAction); 0237 orientationMenu->addAction(orientationVerticalAction); 0238 } 0239 0240 QMenu* BoxPlot::createContextMenu() { 0241 if (!orientationMenu) 0242 initMenus(); 0243 0244 QMenu* menu = WorksheetElement::createContextMenu(); 0245 QAction* visibilityAction = this->visibilityAction(); 0246 0247 // Orientation 0248 Q_D(const BoxPlot); 0249 if (d->orientation == Orientation::Horizontal) 0250 orientationHorizontalAction->setChecked(true); 0251 else 0252 orientationVerticalAction->setChecked(true); 0253 menu->insertMenu(visibilityAction, orientationMenu); 0254 menu->insertSeparator(visibilityAction); 0255 0256 return menu; 0257 } 0258 0259 void BoxPlot::retransform() { 0260 Q_D(BoxPlot); 0261 d->retransform(); 0262 } 0263 0264 void BoxPlot::recalc() { 0265 Q_D(BoxPlot); 0266 d->recalc(); 0267 } 0268 0269 void BoxPlot::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) { 0270 } 0271 0272 /*! 0273 * creates a new spreadsheet having the data with the positions and the values of the bins. 0274 * the new spreadsheet is added to the current folder. 0275 */ 0276 void BoxPlot::createDataSpreadsheet() { 0277 if (dataColumns().isEmpty()) 0278 return; 0279 0280 // create a new spreadsheet for the following 9 metrics: 0281 // index 0282 // 1st quartile 0283 // 3rd quartile 0284 // median 0285 // whiskers min 0286 // whiskers max 0287 // data points count 0288 // outliers count 0289 // far out points count 0290 0291 auto* spreadsheet = new Spreadsheet(i18n("%1 - Data", name())); 0292 spreadsheet->setColumnCount(9); 0293 spreadsheet->setRowCount(dataColumns().count()); 0294 spreadsheet->column(0)->setColumnMode(AbstractColumn::ColumnMode::Integer); 0295 0296 spreadsheet->column(0)->setName(i18n("index")); 0297 spreadsheet->column(1)->setName(i18n("1st quartile")); 0298 spreadsheet->column(2)->setName(i18n("3rd quartile")); 0299 spreadsheet->column(3)->setName(i18n("median")); 0300 spreadsheet->column(4)->setName(i18n("whiskers min")); 0301 spreadsheet->column(5)->setName(i18n("whiskers max")); 0302 spreadsheet->column(6)->setName(i18n("data points count")); 0303 spreadsheet->column(7)->setName(i18n("outliers count")); 0304 spreadsheet->column(8)->setName(i18n("far out points count")); 0305 0306 Q_D(const BoxPlot); 0307 d->fillDataSpreadsheet(spreadsheet); 0308 0309 // add the new spreadsheet to the current folder 0310 folder()->addChild(spreadsheet); 0311 } 0312 0313 /* ============================ getter methods ================= */ 0314 // general 0315 BASIC_SHARED_D_READER_IMPL(BoxPlot, QVector<const AbstractColumn*>, dataColumns, dataColumns) 0316 BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::Ordering, ordering, ordering) 0317 BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::Orientation, orientation, orientation) 0318 BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, variableWidth, variableWidth) 0319 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, widthFactor, widthFactor) 0320 BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, notchesEnabled, notchesEnabled) 0321 0322 // box filling 0323 Background* BoxPlot::backgroundAt(int index) const { 0324 Q_D(const BoxPlot); 0325 if (index < d->backgrounds.size()) 0326 return d->backgrounds.at(index); 0327 else 0328 return nullptr; 0329 } 0330 0331 // box border line 0332 Line* BoxPlot::borderLineAt(int index) const { 0333 Q_D(const BoxPlot); 0334 if (index < d->borderLines.size()) 0335 return d->borderLines.at(index); 0336 else 0337 return nullptr; 0338 } 0339 0340 // median line 0341 Line* BoxPlot::medianLineAt(int index) const { 0342 Q_D(const BoxPlot); 0343 if (index < d->medianLines.size()) 0344 return d->medianLines.at(index); 0345 else 0346 return nullptr; 0347 } 0348 0349 // markers 0350 Symbol* BoxPlot::symbolMean() const { 0351 Q_D(const BoxPlot); 0352 return d->symbolMean; 0353 } 0354 0355 Symbol* BoxPlot::symbolMedian() const { 0356 Q_D(const BoxPlot); 0357 return d->symbolMedian; 0358 } 0359 0360 Symbol* BoxPlot::symbolOutlier() const { 0361 Q_D(const BoxPlot); 0362 return d->symbolOutlier; 0363 } 0364 0365 Symbol* BoxPlot::symbolFarOut() const { 0366 Q_D(const BoxPlot); 0367 return d->symbolFarOut; 0368 } 0369 0370 BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, jitteringEnabled, jitteringEnabled) 0371 0372 Symbol* BoxPlot::symbolData() const { 0373 Q_D(const BoxPlot); 0374 return d->symbolData; 0375 } 0376 0377 Symbol* BoxPlot::symbolWhiskerEnd() const { 0378 Q_D(const BoxPlot); 0379 return d->symbolWhiskerEnd; 0380 } 0381 0382 // whiskers 0383 BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::WhiskersType, whiskersType, whiskersType) 0384 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, whiskersRangeParameter, whiskersRangeParameter) 0385 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, whiskersCapSize, whiskersCapSize) 0386 0387 Line* BoxPlot::whiskersLine() const { 0388 Q_D(const BoxPlot); 0389 return d->whiskersLine; 0390 } 0391 0392 Line* BoxPlot::whiskersCapLine() const { 0393 Q_D(const BoxPlot); 0394 return d->whiskersCapLine; 0395 } 0396 0397 // margin plots 0398 BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, rugEnabled, rugEnabled) 0399 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugLength, rugLength) 0400 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugWidth, rugWidth) 0401 BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugOffset, rugOffset) 0402 0403 QVector<QString>& BoxPlot::dataColumnPaths() const { 0404 D(BoxPlot); 0405 return d->dataColumnPaths; 0406 } 0407 0408 double BoxPlot::minimum(const Dimension dim) const { 0409 Q_D(const BoxPlot); 0410 switch (dim) { 0411 case Dimension::X: 0412 return d->xMin; 0413 case Dimension::Y: 0414 return d->yMin; 0415 } 0416 return NAN; 0417 } 0418 0419 double BoxPlot::maximum(const Dimension dim) const { 0420 Q_D(const BoxPlot); 0421 switch (dim) { 0422 case Dimension::X: 0423 return d->xMax; 0424 case Dimension::Y: 0425 return d->yMax; 0426 } 0427 return NAN; 0428 } 0429 0430 bool BoxPlot::hasData() const { 0431 Q_D(const BoxPlot); 0432 return !d->dataColumns.isEmpty(); 0433 } 0434 0435 bool BoxPlot::usingColumn(const Column* column) const { 0436 Q_D(const BoxPlot); 0437 0438 for (auto* c : d->dataColumns) { 0439 if (c == column) 0440 return true; 0441 } 0442 0443 return false; 0444 } 0445 0446 void BoxPlot::updateColumnDependencies(const AbstractColumn* column) { 0447 Q_D(const BoxPlot); 0448 const QString& columnPath = column->path(); 0449 const auto dataColumnPaths = d->dataColumnPaths; 0450 auto dataColumns = d->dataColumns; 0451 bool changed = false; 0452 0453 for (int i = 0; i < dataColumnPaths.count(); ++i) { 0454 const auto& path = dataColumnPaths.at(i); 0455 0456 if (path == columnPath) { 0457 dataColumns[i] = column; 0458 changed = true; 0459 } 0460 } 0461 0462 if (changed) { 0463 setUndoAware(false); 0464 setDataColumns(dataColumns); 0465 setUndoAware(true); 0466 } 0467 } 0468 0469 QColor BoxPlot::color() const { 0470 // Q_D(const BoxPlot); 0471 return QColor(); 0472 } 0473 0474 /* ============================ setter methods and undo commands ================= */ 0475 0476 // General 0477 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetDataColumns, QVector<const AbstractColumn*>, dataColumns, recalc) 0478 void BoxPlot::setDataColumns(const QVector<const AbstractColumn*> columns) { 0479 Q_D(BoxPlot); 0480 if (columns != d->dataColumns) { 0481 exec(new BoxPlotSetDataColumnsCmd(d, columns, ki18n("%1: set data columns"))); 0482 0483 for (auto* column : columns) { 0484 if (!column) 0485 continue; 0486 0487 connect(column, &AbstractColumn::dataChanged, this, &BoxPlot::dataChanged); 0488 0489 // update the curve itself on changes 0490 connect(column, &AbstractColumn::dataChanged, this, &BoxPlot::recalc); 0491 connect(column, &AbstractAspect::aspectDescriptionChanged, this, &Plot::appearanceChanged); 0492 connect(column->parentAspect(), &AbstractAspect::childAspectAboutToBeRemoved, this, &BoxPlot::dataColumnAboutToBeRemoved); 0493 // TODO: add disconnect in the undo-function 0494 } 0495 } 0496 } 0497 0498 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetOrdering, BoxPlot::Ordering, ordering, recalc) 0499 void BoxPlot::setOrdering(BoxPlot::Ordering ordering) { 0500 Q_D(BoxPlot); 0501 if (ordering != d->ordering) 0502 exec(new BoxPlotSetOrderingCmd(d, ordering, ki18n("%1: set ordering"))); 0503 } 0504 0505 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetOrientation, BoxPlot::Orientation, orientation, recalc) 0506 void BoxPlot::setOrientation(BoxPlot::Orientation orientation) { 0507 Q_D(BoxPlot); 0508 if (orientation != d->orientation) 0509 exec(new BoxPlotSetOrientationCmd(d, orientation, ki18n("%1: set orientation"))); 0510 } 0511 0512 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetVariableWidth, bool, variableWidth, recalc) 0513 void BoxPlot::setVariableWidth(bool variableWidth) { 0514 Q_D(BoxPlot); 0515 if (variableWidth != d->variableWidth) 0516 exec(new BoxPlotSetVariableWidthCmd(d, variableWidth, ki18n("%1: variable width changed"))); 0517 } 0518 0519 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWidthFactor, double, widthFactor, recalc) 0520 void BoxPlot::setWidthFactor(double widthFactor) { 0521 Q_D(BoxPlot); 0522 if (widthFactor != d->widthFactor) 0523 exec(new BoxPlotSetWidthFactorCmd(d, widthFactor, ki18n("%1: width factor changed"))); 0524 } 0525 0526 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetNotchesEnabled, bool, notchesEnabled, recalc) 0527 void BoxPlot::setNotchesEnabled(bool notchesEnabled) { 0528 Q_D(BoxPlot); 0529 if (notchesEnabled != d->notchesEnabled) 0530 exec(new BoxPlotSetNotchesEnabledCmd(d, notchesEnabled, ki18n("%1: changed notches"))); 0531 } 0532 0533 // whiskers 0534 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersType, BoxPlot::WhiskersType, whiskersType, recalc) 0535 void BoxPlot::setWhiskersType(BoxPlot::WhiskersType type) { 0536 Q_D(BoxPlot); 0537 if (type != d->whiskersType) 0538 exec(new BoxPlotSetWhiskersTypeCmd(d, type, ki18n("%1: set whiskers type"))); 0539 } 0540 0541 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersRangeParameter, double, whiskersRangeParameter, recalc) 0542 void BoxPlot::setWhiskersRangeParameter(double k) { 0543 Q_D(BoxPlot); 0544 if (k != d->whiskersRangeParameter) 0545 exec(new BoxPlotSetWhiskersRangeParameterCmd(d, k, ki18n("%1: set whiskers range parameter"))); 0546 } 0547 0548 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersCapSize, double, whiskersCapSize, recalc) 0549 void BoxPlot::setWhiskersCapSize(double size) { 0550 Q_D(BoxPlot); 0551 if (size != d->whiskersCapSize) 0552 exec(new BoxPlotSetWhiskersCapSizeCmd(d, size, ki18n("%1: set whiskers cap size"))); 0553 } 0554 0555 // Symbols 0556 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetJitteringEnabled, bool, jitteringEnabled, recalc) 0557 void BoxPlot::setJitteringEnabled(bool enabled) { 0558 Q_D(BoxPlot); 0559 if (enabled != d->jitteringEnabled) 0560 exec(new BoxPlotSetJitteringEnabledCmd(d, enabled, ki18n("%1: jitterring changed"))); 0561 } 0562 0563 // margin plots 0564 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugEnabled, bool, rugEnabled, updateRug) 0565 void BoxPlot::setRugEnabled(bool enabled) { 0566 Q_D(BoxPlot); 0567 if (enabled != d->rugEnabled) 0568 exec(new BoxPlotSetRugEnabledCmd(d, enabled, ki18n("%1: change rug enabled"))); 0569 } 0570 0571 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugWidth, double, rugWidth, updatePixmap) 0572 void BoxPlot::setRugWidth(double width) { 0573 Q_D(BoxPlot); 0574 if (width != d->rugWidth) 0575 exec(new BoxPlotSetRugWidthCmd(d, width, ki18n("%1: change rug width"))); 0576 } 0577 0578 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugLength, double, rugLength, updateRug) 0579 void BoxPlot::setRugLength(double length) { 0580 Q_D(BoxPlot); 0581 if (length != d->rugLength) 0582 exec(new BoxPlotSetRugLengthCmd(d, length, ki18n("%1: change rug length"))); 0583 } 0584 0585 STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugOffset, double, rugOffset, updateRug) 0586 void BoxPlot::setRugOffset(double offset) { 0587 Q_D(BoxPlot); 0588 if (offset != d->rugOffset) 0589 exec(new BoxPlotSetRugOffsetCmd(d, offset, ki18n("%1: change rug offset"))); 0590 } 0591 0592 // ############################################################################## 0593 // ################################# SLOTS #################################### 0594 // ############################################################################## 0595 0596 void BoxPlot::dataColumnAboutToBeRemoved(const AbstractAspect* aspect) { 0597 Q_D(BoxPlot); 0598 for (int i = 0; i < d->dataColumns.size(); ++i) { 0599 if (aspect == d->dataColumns.at(i)) { 0600 d->dataColumns[i] = nullptr; 0601 d->retransform(); 0602 break; 0603 } 0604 } 0605 } 0606 0607 // ############################################################################## 0608 // ###### SLOTs for changes triggered via QActions in the context menu ######## 0609 // ############################################################################## 0610 void BoxPlot::orientationChangedSlot(QAction* action) { 0611 if (action == orientationHorizontalAction) 0612 this->setOrientation(Axis::Orientation::Horizontal); 0613 else 0614 this->setOrientation(Axis::Orientation::Vertical); 0615 } 0616 0617 // ############################################################################## 0618 // ####################### Private implementation ############################### 0619 // ############################################################################## 0620 BoxPlotPrivate::BoxPlotPrivate(BoxPlot* owner) 0621 : PlotPrivate(owner) 0622 , q(owner) { 0623 setFlag(QGraphicsItem::ItemIsSelectable); 0624 setAcceptHoverEvents(false); 0625 } 0626 0627 Background* BoxPlotPrivate::addBackground(const KConfigGroup& group) { 0628 auto* background = new Background(QString()); 0629 background->setPrefix(QLatin1String("Filling")); 0630 background->setEnabledAvailable(true); 0631 background->setHidden(true); 0632 q->addChild(background); 0633 0634 if (!q->isLoading()) 0635 background->init(group); 0636 0637 q->connect(background, &Background::updateRequested, [=] { 0638 updatePixmap(); 0639 // TODO: Q_EMIT q->updateLegendRequested(); 0640 }); 0641 0642 backgrounds << background; 0643 0644 return background; 0645 } 0646 0647 Line* BoxPlotPrivate::addBorderLine(const KConfigGroup& group) { 0648 auto* line = new Line(QString()); 0649 line->setPrefix(QLatin1String("Border")); 0650 line->setHidden(true); 0651 q->addChild(line); 0652 if (!q->isLoading()) 0653 line->init(group); 0654 0655 q->connect(line, &Line::updatePixmapRequested, [=] { 0656 updatePixmap(); 0657 // Q_EMIT q->updateLegendRequested(); 0658 }); 0659 0660 q->connect(line, &Line::updateRequested, [=] { 0661 recalcShapeAndBoundingRect(); 0662 // Q_EMIT q->updateLegendRequested(); 0663 }); 0664 0665 borderLines << line; 0666 0667 return line; 0668 } 0669 0670 Line* BoxPlotPrivate::addMedianLine(const KConfigGroup& group) { 0671 auto* line = new Line(QString()); 0672 line->setPrefix(QLatin1String("MedianLine")); 0673 line->setHidden(true); 0674 q->addChild(line); 0675 if (!q->isLoading()) 0676 line->init(group); 0677 0678 q->connect(line, &Line::updatePixmapRequested, [=] { 0679 updatePixmap(); 0680 // Q_EMIT q->updateLegendRequested(); 0681 }); 0682 0683 q->connect(line, &Line::updateRequested, [=] { 0684 recalcShapeAndBoundingRect(); 0685 // Q_EMIT q->updateLegendRequested(); 0686 }); 0687 0688 medianLines << line; 0689 0690 return line; 0691 } 0692 0693 void BoxPlotPrivate::fillDataSpreadsheet(Spreadsheet* spreadsheet) const { 0694 // index 0695 // 1st quartile 0696 // 3rd quartile 0697 // median 0698 // whiskers min 0699 // whiskers max 0700 // data points count 0701 // outliers count 0702 // far out points count 0703 0704 for (int i = 0; i < q->dataColumns().count(); ++i) { 0705 const auto* column = static_cast<const Column*>(q->dataColumns().at(i)); 0706 const auto& statistics = column->statistics(); 0707 0708 spreadsheet->column(0)->setIntegerAt(i, i + 1); 0709 spreadsheet->column(1)->setValueAt(i, statistics.firstQuartile); 0710 spreadsheet->column(2)->setValueAt(i, statistics.thirdQuartile); 0711 spreadsheet->column(3)->setValueAt(i, statistics.median); 0712 spreadsheet->column(4)->setValueAt(i, m_whiskerMin.at(i)); 0713 spreadsheet->column(5)->setValueAt(i, m_whiskerMax.at(i)); 0714 spreadsheet->column(6)->setValueAt(i, m_dataPointsLogical.at(i).count()); 0715 spreadsheet->column(7)->setValueAt(i, m_outlierPointsLogical.at(i).count()); 0716 spreadsheet->column(8)->setValueAt(i, m_farOutPointsLogical.at(i).count()); 0717 } 0718 } 0719 0720 /*! 0721 * adds additional elements for background and line properties according to the current 0722 * number of data columns to be plotted. 0723 */ 0724 void BoxPlotPrivate::adjustPropertiesContainers() { 0725 int diff = dataColumns.size() - backgrounds.size(); 0726 if (diff > 0) { 0727 // one more box needs to be added 0728 KConfig config; 0729 KConfigGroup group = config.group(QLatin1String("XYCurve")); 0730 const auto* plot = static_cast<const CartesianPlot*>(q->parentAspect()); 0731 0732 for (int i = 0; i < diff; ++i) { 0733 // box filling and border line 0734 auto* background = addBackground(group); 0735 auto* borderLine = addBorderLine(group); 0736 auto* medianLine = addMedianLine(group); 0737 0738 if (plot) { 0739 const auto& themeColor = plot->themeColorPalette(backgrounds.count() - 1); 0740 background->setFirstColor(themeColor); 0741 borderLine->setColor(themeColor); 0742 medianLine->setColor(themeColor); 0743 } 0744 } 0745 } 0746 } 0747 0748 /*! 0749 called when the size of the plot or its data ranges (manual changes, zooming, etc.) were changed. 0750 recalculates the position of the scene points to be drawn. 0751 triggers the update of lines, drop lines, symbols etc. 0752 */ 0753 void BoxPlotPrivate::retransform() { 0754 const bool suppressed = suppressRetransform || !isVisible() || q->isLoading(); 0755 Q_EMIT trackRetransformCalled(suppressed); 0756 if (suppressed) 0757 return; 0758 0759 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 0760 0761 const int count = dataColumns.size(); 0762 if (!count || m_boxRect.size() != count) { 0763 // no columns or recalc() was not called yet, nothing to do 0764 recalcShapeAndBoundingRect(); 0765 return; 0766 } 0767 0768 // clear the containers holding the information in scene coordinates 0769 for (int i = 0; i < count; ++i) { 0770 m_boxRect[i].clear(); 0771 m_medianLine[i] = QLineF(); 0772 m_whiskersPath[i] = QPainterPath(); 0773 m_whiskersCapPath[i] = QPainterPath(); 0774 m_rugPath[i] = QPainterPath(); 0775 m_outlierPoints[i].clear(); 0776 m_dataPoints[i].clear(); 0777 m_farOutPoints[i].clear(); 0778 m_whiskerEndPoints[i].clear(); 0779 } 0780 0781 if (count) { 0782 if (orientation == BoxPlot::Orientation::Vertical) { 0783 for (int i = 0; i < count; ++i) { 0784 if (dataColumns.at(i)) 0785 verticalBoxPlot(i); 0786 } 0787 } else { 0788 for (int i = 0; i < count; ++i) { 0789 if (dataColumns.at(i)) 0790 horizontalBoxPlot(i); 0791 } 0792 } 0793 0794 updateRug(); 0795 } 0796 0797 recalcShapeAndBoundingRect(); 0798 } 0799 0800 void BoxPlotPrivate::recalc() { 0801 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 0802 0803 // resize the internal containers 0804 const int count = dataColumns.size(); 0805 m_boxRect.resize(count); 0806 m_fillPolygon.resize(count); 0807 m_xMinBox.resize(count); 0808 m_xMaxBox.resize(count); 0809 m_yMinBox.resize(count); 0810 m_yMaxBox.resize(count); 0811 m_median.resize(count); 0812 m_medianLine.resize(count); 0813 m_whiskersPath.resize(count); 0814 m_whiskersCapPath.resize(count); 0815 m_rugPath.resize(count); 0816 m_whiskerMin.resize(count); 0817 m_whiskerMax.resize(count); 0818 m_outlierPointsLogical.resize(count); 0819 m_outlierPoints.resize(count); 0820 m_dataPointsLogical.resize(count); 0821 m_dataPoints.resize(count); 0822 m_farOutPointsLogical.resize(count); 0823 m_farOutPoints.resize(count); 0824 m_whiskerEndPointsLogical.resize(count); 0825 m_whiskerEndPoints.resize(count); 0826 m_mean.resize(count); 0827 m_meanPointLogical.resize(count); 0828 m_meanPoint.resize(count); 0829 m_meanPointVisible.resize(count); 0830 m_medianPointLogical.resize(count); 0831 m_medianPoint.resize(count); 0832 m_medianPointVisible.resize(count); 0833 0834 // box properties 0835 int diff = count - backgrounds.size(); 0836 if (diff > 0) 0837 adjustPropertiesContainers(); 0838 else if (diff < 0) { 0839 // the last box was deleted 0840 // TODO: 0841 } 0842 0843 const double xMinOld = xMin; 0844 const double xMaxOld = xMax; 0845 const double yMinOld = yMin; 0846 const double yMaxOld = yMax; 0847 0848 // calculate the new min and max values of the box plot 0849 // for the current sizes of the box and of the whiskers 0850 if (orientation == BoxPlot::Orientation::Vertical) { 0851 xMin = 0.5; 0852 xMax = count + 0.5; 0853 yMin = INFINITY; 0854 yMax = -INFINITY; 0855 } else { // horizontal 0856 xMin = INFINITY; 0857 xMax = -INFINITY; 0858 yMin = 0.5; 0859 yMax = count + 0.5; 0860 } 0861 0862 if (variableWidth) { 0863 m_widthScaleFactor = -INFINITY; 0864 for (const auto* col : dataColumns) { 0865 auto* column = static_cast<const Column*>(col); 0866 if (column->statistics().size > m_widthScaleFactor) 0867 m_widthScaleFactor = column->statistics().size; 0868 } 0869 m_widthScaleFactor = std::sqrt(m_widthScaleFactor); 0870 } 0871 0872 if (ordering == BoxPlot::Ordering::None) 0873 dataColumnsOrdered = dataColumns; 0874 else { 0875 std::vector<std::pair<double, int>> newOrdering; 0876 0877 if (ordering == BoxPlot::Ordering::MedianAscending || ordering == BoxPlot::Ordering::MedianDescending) { 0878 for (int i = 0; i < count; ++i) { 0879 auto* column = static_cast<const Column*>(dataColumns.at(i)); 0880 newOrdering.push_back(std::make_pair(column->statistics().median, i)); 0881 } 0882 } else { 0883 for (int i = 0; i < count; ++i) { 0884 auto* column = static_cast<const Column*>(dataColumns.at(i)); 0885 newOrdering.push_back(std::make_pair(column->statistics().arithmeticMean, i)); 0886 } 0887 } 0888 0889 std::sort(newOrdering.begin(), newOrdering.end()); 0890 dataColumnsOrdered.clear(); 0891 0892 if (ordering == BoxPlot::Ordering::MedianAscending || ordering == BoxPlot::Ordering::MeanAscending) { 0893 for (int i = 0; i < count; ++i) 0894 dataColumnsOrdered << dataColumns.at(newOrdering.at(i).second); 0895 } else { 0896 for (int i = count - 1; i >= 0; --i) 0897 dataColumnsOrdered << dataColumns.at(newOrdering.at(i).second); 0898 } 0899 } 0900 0901 for (int i = 0; i < count; ++i) 0902 recalc(i); 0903 0904 // if the size of the plot has changed because of the actual 0905 // data changes or because of new plot settings, emit dataChanged() 0906 // in order to recalculate the data ranges in the parent plot area 0907 // and to retransform all its children. 0908 // Just call retransform() to update the plot only if the ranges didn't change. 0909 if (xMin != xMinOld || xMax != xMaxOld || yMin != yMinOld || yMax != yMaxOld) 0910 Q_EMIT q->dataChanged(); 0911 else 0912 retransform(); 0913 } 0914 0915 QPointF BoxPlotPrivate::setOutlierPoint(double pos, double value) { 0916 QPointF point; 0917 if (orientation == BoxPlot::Orientation::Vertical) { 0918 point.setX(pos); 0919 point.setY(value); 0920 0921 if (value > yMax) 0922 yMax = value; 0923 else if (value < yMin) 0924 yMin = value; 0925 } else { 0926 point.setX(value); 0927 point.setY(pos); 0928 0929 if (value > xMax) 0930 xMax = value; 0931 else if (value < xMin) 0932 xMin = value; 0933 } 0934 0935 return point; 0936 } 0937 0938 void BoxPlotPrivate::recalc(int index) { 0939 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 0940 auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index)); 0941 if (!column) 0942 return; 0943 0944 // clear the containers for outliers, etc. since their number 0945 // can be changed because of the new settings for whiskers, etc. 0946 m_outlierPointsLogical[index].clear(); 0947 m_outlierPoints[index].clear(); 0948 m_dataPointsLogical[index].clear(); 0949 m_dataPoints[index].clear(); 0950 m_farOutPointsLogical[index].clear(); 0951 m_farOutPoints[index].clear(); 0952 m_whiskerEndPointsLogical[index].clear(); 0953 m_whiskerEndPoints[index].clear(); 0954 0955 const auto& statistics = column->statistics(); 0956 double width = 0.5 * widthFactor; 0957 if (variableWidth && m_widthScaleFactor != 0) 0958 width *= std::sqrt(statistics.size) / m_widthScaleFactor; 0959 0960 const double x = index + 1.0; 0961 0962 // box 0963 if (orientation == BoxPlot::Orientation::Vertical) { 0964 m_xMinBox[index] = x - width / 2; 0965 m_xMaxBox[index] = x + width / 2; 0966 m_yMinBox[index] = statistics.firstQuartile; 0967 m_yMaxBox[index] = statistics.thirdQuartile; 0968 } else { 0969 m_xMinBox[index] = statistics.firstQuartile; 0970 m_xMaxBox[index] = statistics.thirdQuartile; 0971 m_yMinBox[index] = x - width / 2; 0972 m_yMaxBox[index] = x + width / 2; 0973 } 0974 0975 // mean and median 0976 m_median[index] = statistics.median; 0977 m_mean[index] = statistics.arithmeticMean; 0978 0979 // whiskers 0980 switch (whiskersType) { 0981 case BoxPlot::WhiskersType::MinMax: { 0982 m_whiskerMax[index] = statistics.maximum; 0983 m_whiskerMin[index] = statistics.minimum; 0984 break; 0985 } 0986 case BoxPlot::WhiskersType::IQR: { 0987 m_whiskerMax[index] = statistics.thirdQuartile + whiskersRangeParameter * statistics.iqr; 0988 m_whiskerMin[index] = statistics.firstQuartile - whiskersRangeParameter * statistics.iqr; 0989 break; 0990 } 0991 case BoxPlot::WhiskersType::SD: { 0992 m_whiskerMax[index] = statistics.arithmeticMean + whiskersRangeParameter * statistics.standardDeviation; 0993 m_whiskerMin[index] = statistics.arithmeticMean - whiskersRangeParameter * statistics.standardDeviation; 0994 break; 0995 } 0996 case BoxPlot::WhiskersType::MAD: { 0997 m_whiskerMax[index] = statistics.median + whiskersRangeParameter * statistics.meanDeviationAroundMedian; 0998 m_whiskerMin[index] = statistics.median - whiskersRangeParameter * statistics.meanDeviationAroundMedian; 0999 break; 1000 } 1001 case BoxPlot::WhiskersType::PERCENTILES_1_99: { 1002 m_whiskerMax[index] = statistics.percentile_99; 1003 m_whiskerMin[index] = statistics.percentile_1; 1004 break; 1005 } 1006 case BoxPlot::WhiskersType::PERCENTILES_5_95: { 1007 m_whiskerMax[index] = statistics.percentile_95; 1008 m_whiskerMin[index] = statistics.percentile_5; 1009 break; 1010 } 1011 case BoxPlot::WhiskersType::PERCENTILES_10_90: { 1012 m_whiskerMax[index] = statistics.percentile_90; 1013 m_whiskerMin[index] = statistics.percentile_10; 1014 break; 1015 } 1016 } 1017 1018 // outliers symbols 1019 if (orientation == BoxPlot::Orientation::Vertical) { 1020 if (m_whiskerMax[index] > yMax) 1021 yMax = m_whiskerMax[index]; 1022 if (m_whiskerMin[index] < yMin) 1023 yMin = m_whiskerMin[index]; 1024 } else { 1025 if (m_whiskerMax[index] > xMax) 1026 xMax = m_whiskerMax[index]; 1027 if (m_whiskerMin[index] < xMin) 1028 xMin = m_whiskerMin[index]; 1029 } 1030 1031 double whiskerMax = -INFINITY; // upper adjacent value 1032 double whiskerMin = INFINITY; // lower adjacent value 1033 const double outerFenceMax = statistics.thirdQuartile + 3.0 * statistics.iqr; 1034 const double outerFenceMin = statistics.firstQuartile - 3.0 * statistics.iqr; 1035 1036 for (int row = 0; row < column->rowCount(); ++row) { 1037 if (!column->isValid(row) || column->isMasked(row)) 1038 continue; 1039 1040 double value = 0.0; 1041 switch (column->columnMode()) { 1042 case AbstractColumn::ColumnMode::Double: 1043 case AbstractColumn::ColumnMode::Integer: 1044 case AbstractColumn::ColumnMode::BigInt: 1045 value = column->valueAt(row); 1046 break; 1047 case AbstractColumn::ColumnMode::DateTime: 1048 value = column->dateTimeAt(row).toMSecsSinceEpoch(); 1049 break; 1050 case AbstractColumn::ColumnMode::Text: 1051 case AbstractColumn::ColumnMode::Month: 1052 case AbstractColumn::ColumnMode::Day: 1053 break; 1054 } 1055 1056 double rand = 0.5; 1057 if (jitteringEnabled) 1058 rand = (double)std::rand() / ((double)RAND_MAX + 1); 1059 1060 if (value > m_whiskerMax.at(index) || value < m_whiskerMin.at(index)) { 1061 if (whiskersType == BoxPlot::WhiskersType::IQR && (value > outerFenceMax || value < outerFenceMin)) 1062 m_farOutPointsLogical[index] << setOutlierPoint(x - width / 2 + rand * width, value); 1063 else 1064 m_outlierPointsLogical[index] << setOutlierPoint(x - width / 2 + rand * width, value); 1065 } else { 1066 if (orientation == BoxPlot::Orientation::Vertical) 1067 m_dataPointsLogical[index] << QPointF(x - width / 2 + rand * width, value); 1068 else 1069 m_dataPointsLogical[index] << QPointF(value, x - width / 2 + rand * width); 1070 1071 // determine the upper/lower adjucent values 1072 if (whiskersType == BoxPlot::WhiskersType::IQR) { 1073 if (value > whiskerMax) 1074 whiskerMax = value; 1075 1076 if (value < whiskerMin) 1077 whiskerMin = value; 1078 } 1079 } 1080 } 1081 1082 // set the whisker ends at the upper and lower adjacent values 1083 if (whiskersType == BoxPlot::WhiskersType::IQR) { 1084 if (whiskerMax != -INFINITY) 1085 m_whiskerMax[index] = whiskerMax; 1086 if (whiskerMin != INFINITY) 1087 m_whiskerMin[index] = whiskerMin; 1088 } 1089 } 1090 1091 void BoxPlotPrivate::verticalBoxPlot(int index) { 1092 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 1093 1094 QVector<QLineF> lines; 1095 const double x = index + 1.0; 1096 const double xMinBox = m_xMinBox.at(index); 1097 const double xMaxBox = m_xMaxBox.at(index); 1098 const double yMinBox = m_yMinBox.at(index); 1099 const double yMaxBox = m_yMaxBox.at(index); 1100 const double median = m_median.at(index); 1101 1102 // box 1103 if (!notchesEnabled) { 1104 // first line starting at the top left corner of the box 1105 lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox); 1106 lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox); 1107 lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox); 1108 lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox); 1109 } else { 1110 auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index)); 1111 const auto& statistics = column->statistics(); 1112 const double notch = 1.7 * 1.25 * statistics.iqr / 1.35 / std::sqrt(statistics.size); 1113 const double notchMax = median + notch; 1114 const double notchMin = median - notch; 1115 double width = xMaxBox - xMinBox; 1116 1117 // first line starting at the top left corner of the box 1118 lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox); 1119 lines << QLineF(xMaxBox, yMaxBox, xMaxBox, notchMax); 1120 lines << QLineF(xMaxBox, notchMax, xMinBox + 0.9 * width, median); 1121 lines << QLineF(xMinBox + 0.9 * width, median, xMaxBox, notchMin); 1122 lines << QLineF(xMaxBox, notchMin, xMaxBox, yMinBox); 1123 lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox); 1124 lines << QLineF(xMinBox, yMinBox, xMinBox, notchMin); 1125 lines << QLineF(xMinBox, notchMin, xMinBox + 0.1 * width, median); 1126 lines << QLineF(xMinBox + 0.1 * width, median, xMinBox, notchMax); 1127 lines << QLineF(xMinBox, notchMax, xMinBox, yMaxBox); 1128 } 1129 1130 m_boxRect[index] = q->cSystem->mapLogicalToScene(lines); 1131 updateFillingRect(index, lines); 1132 1133 // median line 1134 lines.clear(); 1135 if (!notchesEnabled) 1136 lines << QLineF(xMinBox, median, xMaxBox, median); 1137 else { 1138 double width = xMaxBox - xMinBox; 1139 lines << QLineF(xMinBox + 0.1 * width, median, m_xMaxBox.at(index) - 0.1 * width, median); 1140 } 1141 1142 lines = q->cSystem->mapLogicalToScene(lines); 1143 if (!lines.isEmpty()) 1144 m_medianLine[index] = lines.first(); 1145 1146 // whisker lines 1147 lines.clear(); 1148 lines << QLineF(x, m_yMaxBox.at(index), x, m_whiskerMax.at(index)); // upper whisker 1149 lines << QLineF(x, m_yMinBox.at(index), x, m_whiskerMin.at(index)); // lower whisker 1150 lines = q->cSystem->mapLogicalToScene(lines); 1151 for (const auto& line : qAsConst(lines)) { 1152 m_whiskersPath[index].moveTo(line.p1()); 1153 m_whiskersPath[index].lineTo(line.p2()); 1154 } 1155 1156 // whisker caps 1157 if (!m_whiskersPath[index].isEmpty()) { 1158 bool visible = false; 1159 QPointF maxPoint = q->cSystem->mapLogicalToScene(QPointF(x, m_whiskerMax.at(index)), visible); 1160 if (visible) { 1161 m_whiskersCapPath[index].moveTo(QPointF(maxPoint.x() - whiskersCapSize / 2., maxPoint.y())); 1162 m_whiskersCapPath[index].lineTo(QPointF(maxPoint.x() + whiskersCapSize / 2., maxPoint.y())); 1163 m_whiskerEndPointsLogical[index] << QPointF(x, m_whiskerMax.at(index)); 1164 } 1165 1166 QPointF minPoint = q->cSystem->mapLogicalToScene(QPointF(x, m_whiskerMin.at(index)), visible); 1167 if (visible) { 1168 m_whiskersCapPath[index].moveTo(QPointF(minPoint.x() - whiskersCapSize / 2., minPoint.y())); 1169 m_whiskersCapPath[index].lineTo(QPointF(minPoint.x() + whiskersCapSize / 2., minPoint.y())); 1170 m_whiskerEndPointsLogical[index] << QPointF(x, m_whiskerMin.at(index)); 1171 } 1172 } 1173 1174 // map the logical points of symbols to scene coordinates 1175 m_meanPointLogical[index] = QPointF(x, m_mean.at(index)); 1176 m_medianPointLogical[index] = QPointF(x, m_median.at(index)); 1177 mapSymbolsToScene(index); 1178 } 1179 1180 void BoxPlotPrivate::horizontalBoxPlot(int index) { 1181 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 1182 1183 QVector<QLineF> lines; 1184 const double y = index + 1.0; 1185 const double xMinBox = m_xMinBox.at(index); 1186 const double xMaxBox = m_xMaxBox.at(index); 1187 const double yMinBox = m_yMinBox.at(index); 1188 const double yMaxBox = m_yMaxBox.at(index); 1189 const double median = m_median.at(index); 1190 1191 // box 1192 if (!notchesEnabled) { 1193 // first line starting at the top left corner of the box 1194 lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox); 1195 lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox); 1196 lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox); 1197 lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox); 1198 } else { 1199 auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index)); 1200 const auto& statistics = column->statistics(); 1201 const double notch = 1.7 * 1.25 * statistics.iqr / 1.35 / std::sqrt(statistics.size); 1202 const double notchMax = median + notch; 1203 const double notchMin = median - notch; 1204 double width = yMaxBox - yMinBox; 1205 1206 lines << QLineF(xMinBox, yMaxBox, notchMin, yMaxBox); 1207 lines << QLineF(notchMin, yMaxBox, median, yMaxBox - 0.1 * width); 1208 lines << QLineF(median, yMaxBox - 0.1 * width, notchMax, yMaxBox); 1209 lines << QLineF(notchMax, yMaxBox, xMaxBox, yMaxBox); 1210 lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox); 1211 lines << QLineF(xMaxBox, yMinBox, notchMax, yMinBox); 1212 lines << QLineF(notchMax, yMinBox, median, yMinBox + 0.1 * width); 1213 lines << QLineF(median, yMinBox + 0.1 * width, notchMin, yMinBox); 1214 lines << QLineF(notchMin, yMinBox, xMinBox, yMinBox); 1215 lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox); 1216 } 1217 1218 m_boxRect[index] = q->cSystem->mapLogicalToScene(lines); 1219 updateFillingRect(index, lines); 1220 1221 // median line 1222 lines.clear(); 1223 if (!notchesEnabled) 1224 lines << QLineF(median, yMinBox, median, yMaxBox); 1225 else { 1226 double width = yMaxBox - yMinBox; 1227 lines << QLineF(median, yMinBox + 0.1 * width, median, yMaxBox - 0.1 * width); 1228 } 1229 1230 lines = q->cSystem->mapLogicalToScene(lines); 1231 if (!lines.isEmpty()) 1232 m_medianLine[index] = lines.first(); 1233 1234 // whisker lines 1235 lines.clear(); 1236 lines << QLineF(m_xMaxBox.at(index), y, m_whiskerMax.at(index), y); // upper whisker 1237 lines << QLineF(m_xMinBox.at(index), y, m_whiskerMin.at(index), y); // lower whisker 1238 lines = q->cSystem->mapLogicalToScene(lines); 1239 for (const auto& line : qAsConst(lines)) { 1240 m_whiskersPath[index].moveTo(line.p1()); 1241 m_whiskersPath[index].lineTo(line.p2()); 1242 } 1243 1244 // whisker caps 1245 if (!m_whiskersPath[index].isEmpty()) { 1246 bool visible = false; 1247 QPointF maxPoint = q->cSystem->mapLogicalToScene(QPointF(m_whiskerMax.at(index), y), visible); 1248 if (visible) { 1249 m_whiskersCapPath[index].moveTo(QPointF(maxPoint.x(), maxPoint.y() - whiskersCapSize / 2)); 1250 m_whiskersCapPath[index].lineTo(QPointF(maxPoint.x(), maxPoint.y() + whiskersCapSize / 2)); 1251 m_whiskerEndPointsLogical[index] << QPointF(m_whiskerMax.at(index), y); 1252 } 1253 1254 QPointF minPoint = q->cSystem->mapLogicalToScene(QPointF(m_whiskerMin.at(index), y), visible); 1255 if (visible) { 1256 m_whiskersCapPath[index].moveTo(QPointF(minPoint.x(), minPoint.y() - whiskersCapSize / 2)); 1257 m_whiskersCapPath[index].lineTo(QPointF(minPoint.x(), minPoint.y() + whiskersCapSize / 2)); 1258 m_whiskerEndPointsLogical[index] << QPointF(m_whiskerMin.at(index), y); 1259 } 1260 } 1261 1262 // outliers symbols 1263 m_meanPointLogical[index] = QPointF(m_mean.at(index), y); 1264 m_medianPointLogical[index] = QPointF(m_median.at(index), y); 1265 mapSymbolsToScene(index); 1266 } 1267 1268 void BoxPlotPrivate::updateRug() { 1269 if (!rugEnabled || !q->plot()) { 1270 recalcShapeAndBoundingRect(); 1271 return; 1272 } 1273 1274 auto cs = q->plot()->coordinateSystem(q->coordinateSystemIndex()); 1275 const double xMin = q->plot()->range(Dimension::X, cs->index(Dimension::X)).start(); 1276 const double yMin = q->plot()->range(Dimension::Y, cs->index(Dimension::Y)).start(); 1277 1278 QPainterPath rugPath; 1279 QVector<QPointF> points; 1280 1281 for (int i = 0; i < q->dataColumns().count(); ++i) { 1282 const auto* column = static_cast<const Column*>(q->dataColumns().at(i)); 1283 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) 1284 rugPath.clear(); 1285 #else 1286 rugPath = QPainterPath(); 1287 #endif 1288 points.clear(); 1289 1290 if (orientation == BoxPlot::Orientation::Horizontal) { 1291 for (int row = 0; row < column->rowCount(); ++row) { 1292 if (column->isValid(row) && !column->isMasked(row)) 1293 points << QPointF(column->valueAt(row), yMin); 1294 } 1295 1296 // map the points to scene coordinates 1297 points = q->cSystem->mapLogicalToScene(points); 1298 1299 // path for the vertical rug lines 1300 for (const auto& point : qAsConst(points)) { 1301 rugPath.moveTo(point.x(), point.y() - rugOffset); 1302 rugPath.lineTo(point.x(), point.y() - rugOffset - rugLength); 1303 } 1304 } else { // horizontal 1305 for (int row = 0; row < column->rowCount(); ++row) { 1306 if (column->isValid(row) && !column->isMasked(row)) 1307 points << QPointF(xMin, column->valueAt(row)); 1308 } 1309 1310 // map the points to scene coordinates 1311 points = q->cSystem->mapLogicalToScene(points); 1312 1313 // path for the horizontal rug lines 1314 for (const auto& point : qAsConst(points)) { 1315 rugPath.moveTo(point.x() + rugOffset, point.y()); 1316 rugPath.lineTo(point.x() + rugOffset + rugLength, point.y()); 1317 } 1318 } 1319 1320 m_rugPath[i] = rugPath; 1321 } 1322 1323 recalcShapeAndBoundingRect(); 1324 } 1325 1326 void BoxPlotPrivate::updateFillingRect(int index, const QVector<QLineF>& lines) { 1327 const auto& unclippedLines = q->cSystem->mapLogicalToScene(lines, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping); 1328 1329 if (unclippedLines.isEmpty()) { 1330 m_fillPolygon[index] = QPolygonF(); 1331 return; 1332 } 1333 1334 // we have four unclipped lines for the box. 1335 // clip the points to the plot data rect and create a new polygon 1336 // out of them that will be filled out. 1337 QPolygonF polygon; 1338 const QRectF& dataRect = static_cast<CartesianPlot*>(q->parentAspect())->dataRect(); 1339 int i = 0; 1340 for (const auto& line : unclippedLines) { 1341 // clip the first point of the line 1342 QPointF p1 = line.p1(); 1343 if (p1.x() < dataRect.left()) 1344 p1.setX(dataRect.left()); 1345 else if (p1.x() > dataRect.right()) 1346 p1.setX(dataRect.right()); 1347 1348 if (p1.y() < dataRect.top()) 1349 p1.setY(dataRect.top()); 1350 else if (p1.y() > dataRect.bottom()) 1351 p1.setY(dataRect.bottom()); 1352 1353 // clip the second point of the line 1354 QPointF p2 = line.p2(); 1355 if (p2.x() < dataRect.left()) 1356 p2.setX(dataRect.left()); 1357 else if (p2.x() > dataRect.right()) 1358 p2.setX(dataRect.right()); 1359 1360 if (p2.y() < dataRect.top()) 1361 p2.setY(dataRect.top()); 1362 else if (p2.y() > dataRect.bottom()) 1363 p2.setY(dataRect.bottom()); 1364 1365 if (i != unclippedLines.size() - 1) 1366 polygon << p1; 1367 else { 1368 // close the polygon for the last line 1369 polygon << p1; 1370 polygon << p2; 1371 } 1372 1373 ++i; 1374 } 1375 1376 m_fillPolygon[index] = polygon; 1377 } 1378 1379 /*! 1380 * map the outlier points from logical to scene coordinates, 1381 * avoid drawing overlapping points, logic similar to 1382 * //XYCurvePrivate::retransform() 1383 */ 1384 void BoxPlotPrivate::mapSymbolsToScene(int index) { 1385 // outliers 1386 int size = m_outlierPointsLogical[index].size(); 1387 if (size > 0) { 1388 const int startIndex = 0; 1389 const int endIndex = m_outlierPointsLogical[index].size() - 1; 1390 std::vector<bool> m_pointVisible; 1391 m_pointVisible.resize(size); 1392 1393 q->cSystem->mapLogicalToScene(startIndex, endIndex, m_outlierPointsLogical[index], m_outlierPoints[index], m_pointVisible); 1394 } 1395 1396 // data points 1397 size = m_dataPointsLogical[index].size(); 1398 if (size > 0) { 1399 const int startIndex = 0; 1400 const int endIndex = m_dataPointsLogical[index].size() - 1; 1401 std::vector<bool> pointVisible; 1402 pointVisible.resize(size); 1403 1404 q->cSystem->mapLogicalToScene(startIndex, endIndex, m_dataPointsLogical[index], m_dataPoints[index], pointVisible); 1405 } 1406 1407 // far out points 1408 size = m_farOutPointsLogical[index].size(); 1409 if (size > 0) { 1410 const int startIndex = 0; 1411 const int endIndex = m_farOutPointsLogical[index].size() - 1; 1412 std::vector<bool> pointVisible; 1413 pointVisible.resize(size); 1414 1415 q->cSystem->mapLogicalToScene(startIndex, endIndex, m_farOutPointsLogical[index], m_farOutPoints[index], pointVisible); 1416 } 1417 1418 // whisker ends 1419 size = m_whiskerEndPointsLogical[index].size(); 1420 if (size > 0) { 1421 const int startIndex = 0; 1422 const int endIndex = m_whiskerEndPointsLogical[index].size() - 1; 1423 std::vector<bool> pointVisible; 1424 pointVisible.resize(size); 1425 1426 q->cSystem->mapLogicalToScene(startIndex, endIndex, m_whiskerEndPointsLogical[index], m_whiskerEndPoints[index], pointVisible); 1427 } 1428 1429 // mean 1430 bool visible; 1431 m_meanPoint[index] = q->cSystem->mapLogicalToScene(m_meanPointLogical[index], visible); 1432 m_meanPointVisible[index] = visible; 1433 1434 // median 1435 m_medianPoint[index] = q->cSystem->mapLogicalToScene(m_medianPointLogical[index], visible); 1436 m_medianPointVisible[index] = visible; 1437 } 1438 1439 /*! 1440 recalculates the outer bounds and the shape of the item. 1441 */ 1442 void BoxPlotPrivate::recalcShapeAndBoundingRect() { 1443 prepareGeometryChange(); 1444 m_shape = QPainterPath(); 1445 1446 for (int i = 0; i < dataColumnsOrdered.size(); ++i) { 1447 if (!dataColumnsOrdered.at(i) || static_cast<const Column*>(dataColumnsOrdered.at(i))->statistics().size == 0) 1448 continue; 1449 1450 QPainterPath boxPath; 1451 for (const auto& line : qAsConst(m_boxRect.at(i))) { 1452 boxPath.moveTo(line.p1()); 1453 boxPath.lineTo(line.p2()); 1454 } 1455 m_shape.addPath(WorksheetElement::shapeFromPath(boxPath, borderLines.at(i)->pen())); 1456 1457 m_shape.addPath(WorksheetElement::shapeFromPath(m_whiskersPath.at(i), whiskersLine->pen())); 1458 m_shape.addPath(WorksheetElement::shapeFromPath(m_whiskersCapPath.at(i), whiskersCapLine->pen())); 1459 1460 m_shape.addPath(WorksheetElement::shapeFromPath(m_rugPath.at(i), borderLines.at(i)->pen())); 1461 1462 // add symbols outlier, jitter and far out values 1463 QPainterPath symbolsPath = QPainterPath(); 1464 1465 // outlier values 1466 if (symbolOutlier->style() != Symbol::Style::NoSymbols && !m_outlierPoints.at(i).isEmpty()) { 1467 QPainterPath path = Symbol::stylePath(symbolOutlier->style()); 1468 QTransform trafo; 1469 trafo.scale(symbolOutlier->size(), symbolOutlier->size()); 1470 path = trafo.map(path); 1471 trafo.reset(); 1472 1473 if (symbolOutlier->rotationAngle() != 0.) { 1474 trafo.rotate(symbolOutlier->rotationAngle()); 1475 path = trafo.map(path); 1476 } 1477 1478 for (const auto& point : qAsConst(m_outlierPoints.at(i))) { 1479 trafo.reset(); 1480 trafo.translate(point.x(), point.y()); 1481 symbolsPath.addPath(trafo.map(path)); 1482 } 1483 } 1484 1485 // jitter values 1486 if (symbolData->style() != Symbol::Style::NoSymbols && !m_dataPoints.at(i).isEmpty()) { 1487 QPainterPath path = Symbol::stylePath(symbolData->style()); 1488 QTransform trafo; 1489 trafo.scale(symbolData->size(), symbolData->size()); 1490 path = trafo.map(path); 1491 trafo.reset(); 1492 1493 if (symbolData->rotationAngle() != 0.) { 1494 trafo.rotate(symbolData->rotationAngle()); 1495 path = trafo.map(path); 1496 } 1497 1498 for (const auto& point : qAsConst(m_dataPoints.at(i))) { 1499 trafo.reset(); 1500 trafo.translate(point.x(), point.y()); 1501 symbolsPath.addPath(trafo.map(path)); 1502 } 1503 } 1504 1505 // far out values 1506 if (symbolFarOut->style() != Symbol::Style::NoSymbols && !m_farOutPoints.at(i).isEmpty()) { 1507 QPainterPath path = Symbol::stylePath(symbolFarOut->style()); 1508 QTransform trafo; 1509 trafo.scale(symbolFarOut->size(), symbolFarOut->size()); 1510 path = trafo.map(path); 1511 trafo.reset(); 1512 1513 if (symbolFarOut->rotationAngle() != 0.) { 1514 trafo.rotate(symbolFarOut->rotationAngle()); 1515 path = trafo.map(path); 1516 } 1517 1518 for (const auto& point : qAsConst(m_farOutPoints.at(i))) { 1519 trafo.reset(); 1520 trafo.translate(point.x(), point.y()); 1521 symbolsPath.addPath(trafo.map(path)); 1522 } 1523 } 1524 1525 // whisker ends 1526 if (symbolWhiskerEnd->style() != Symbol::Style::NoSymbols && !m_whiskerEndPoints.at(i).isEmpty()) { 1527 QPainterPath path = Symbol::stylePath(symbolWhiskerEnd->style()); 1528 QTransform trafo; 1529 trafo.scale(symbolWhiskerEnd->size(), symbolWhiskerEnd->size()); 1530 path = trafo.map(path); 1531 trafo.reset(); 1532 1533 if (symbolWhiskerEnd->rotationAngle() != 0.) { 1534 trafo.rotate(symbolWhiskerEnd->rotationAngle()); 1535 path = trafo.map(path); 1536 } 1537 1538 for (const auto& point : qAsConst(m_whiskerEndPoints.at(i))) { 1539 trafo.reset(); 1540 trafo.translate(point.x(), point.y()); 1541 symbolsPath.addPath(trafo.map(path)); 1542 } 1543 } 1544 1545 m_shape.addPath(symbolsPath); 1546 } 1547 1548 m_boundingRectangle = m_shape.boundingRect(); 1549 updatePixmap(); 1550 } 1551 1552 void BoxPlotPrivate::updatePixmap() { 1553 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 1554 QPixmap pixmap(m_boundingRectangle.width(), m_boundingRectangle.height()); 1555 if (m_boundingRectangle.width() == 0. || m_boundingRectangle.height() == 0.) { 1556 m_pixmap = pixmap; 1557 m_hoverEffectImageIsDirty = true; 1558 m_selectionEffectImageIsDirty = true; 1559 return; 1560 } 1561 pixmap.fill(Qt::transparent); 1562 QPainter painter(&pixmap); 1563 painter.setRenderHint(QPainter::Antialiasing, true); 1564 painter.translate(-m_boundingRectangle.topLeft()); 1565 1566 draw(&painter); 1567 painter.end(); 1568 1569 m_pixmap = pixmap; 1570 m_hoverEffectImageIsDirty = true; 1571 m_selectionEffectImageIsDirty = true; 1572 Q_EMIT q->changed(); 1573 update(); 1574 } 1575 1576 void BoxPlotPrivate::draw(QPainter* painter) { 1577 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 1578 1579 for (int i = 0; i < dataColumnsOrdered.size(); ++i) { 1580 if (!dataColumnsOrdered.at(i)) 1581 continue; 1582 1583 // no need to draw anything if the column doesn't have any valid values 1584 if (static_cast<const Column*>(dataColumnsOrdered.at(i))->statistics().size == 0) 1585 continue; 1586 1587 if (!m_boxRect.at(i).isEmpty()) { 1588 // draw the box filling 1589 const auto* background = backgrounds.at(i); 1590 if (background->enabled()) { 1591 painter->setOpacity(backgrounds.at(i)->opacity()); 1592 painter->setPen(Qt::NoPen); 1593 const QPolygonF& polygon = m_fillPolygon.at(i); 1594 drawFillingPollygon(polygon, painter, background); 1595 } 1596 1597 // draw the border 1598 const auto* borderLine = borderLines.at(i); 1599 if (borderLine->pen().style() != Qt::NoPen) { 1600 painter->setPen(borderLine->pen()); 1601 painter->setBrush(Qt::NoBrush); 1602 painter->setOpacity(borderLine->opacity()); 1603 for (const auto& line : m_boxRect.at(i)) 1604 painter->drawLine(line); 1605 } 1606 1607 // draw the median line 1608 const auto* medianLine = medianLines.at(i); 1609 if (medianLine->pen().style() != Qt::NoPen) { 1610 painter->setPen(medianLine->pen()); 1611 painter->setBrush(Qt::NoBrush); 1612 painter->setOpacity(medianLine->opacity()); 1613 painter->drawLine(m_medianLine.at(i)); 1614 } 1615 } 1616 1617 // draw the whiskers 1618 if (whiskersLine->pen().style() != Qt::NoPen && !m_whiskersPath.at(i).isEmpty()) { 1619 painter->setPen(whiskersLine->pen()); 1620 painter->setBrush(Qt::NoBrush); 1621 painter->setOpacity(whiskersLine->opacity()); 1622 painter->drawPath(m_whiskersPath.at(i)); 1623 } 1624 1625 // draw the whiskers cap 1626 if (whiskersCapLine->pen().style() != Qt::NoPen && !m_whiskersCapPath.at(i).isEmpty()) { 1627 painter->setPen(whiskersCapLine->pen()); 1628 painter->setBrush(Qt::NoBrush); 1629 painter->setOpacity(whiskersCapLine->opacity()); 1630 painter->drawPath(m_whiskersCapPath.at(i)); 1631 } 1632 1633 // draw rug 1634 if (rugEnabled && !m_rugPath.at(i).isEmpty()) { 1635 QPen pen; 1636 pen.setColor(borderLines.at(i)->pen().color()); 1637 pen.setWidthF(rugWidth); 1638 painter->setPen(pen); 1639 painter->setOpacity(borderLines.at(i)->opacity()); 1640 painter->drawPath(m_rugPath.at(i)); 1641 } 1642 1643 // draw the symbols 1644 drawSymbols(painter, i); 1645 } 1646 } 1647 1648 void BoxPlotPrivate::drawSymbols(QPainter* painter, int index) { 1649 // outlier values 1650 symbolOutlier->draw(painter, m_outlierPoints.at(index)); 1651 1652 // mean value 1653 if (m_meanPointVisible.at(index)) 1654 symbolMean->draw(painter, m_meanPoint.at(index)); 1655 1656 // median value 1657 if (m_medianPointVisible.at(index)) 1658 symbolMedian->draw(painter, m_medianPoint.at(index)); 1659 1660 // jitter values 1661 symbolData->draw(painter, m_dataPoints.at(index)); 1662 1663 // far out values 1664 symbolFarOut->draw(painter, m_farOutPoints.at(index)); 1665 1666 // whisker end points 1667 symbolWhiskerEnd->draw(painter, m_whiskerEndPoints.at(index)); 1668 } 1669 1670 void BoxPlotPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) { 1671 if (!isVisible()) 1672 return; 1673 1674 painter->setPen(Qt::NoPen); 1675 painter->setBrush(Qt::NoBrush); 1676 painter->setRenderHint(QPainter::SmoothPixmapTransform, true); 1677 1678 if (Settings::group(QStringLiteral("Settings_Worksheet")).readEntry<bool>("DoubleBuffering", true)) 1679 painter->drawPixmap(m_boundingRectangle.topLeft(), m_pixmap); // draw the cached pixmap (fast) 1680 else 1681 draw(painter); // draw directly again (slow) 1682 1683 if (m_hovered && !isSelected() && !q->isPrinting()) { 1684 if (m_hoverEffectImageIsDirty) { 1685 QPixmap pix = m_pixmap; 1686 QPainter p(&pix); 1687 p.setCompositionMode(QPainter::CompositionMode_SourceIn); // source (shadow) pixels merged with the alpha channel of the destination (m_pixmap) 1688 p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Shadow)); 1689 p.end(); 1690 1691 m_hoverEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); 1692 m_hoverEffectImageIsDirty = false; 1693 } 1694 1695 painter->drawImage(m_boundingRectangle.topLeft(), m_hoverEffectImage, m_pixmap.rect()); 1696 return; 1697 } 1698 1699 if (isSelected() && !q->isPrinting()) { 1700 if (m_selectionEffectImageIsDirty) { 1701 QPixmap pix = m_pixmap; 1702 QPainter p(&pix); 1703 p.setCompositionMode(QPainter::CompositionMode_SourceIn); 1704 p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Highlight)); 1705 p.end(); 1706 1707 m_selectionEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); 1708 m_selectionEffectImageIsDirty = false; 1709 } 1710 1711 painter->drawImage(m_boundingRectangle.topLeft(), m_selectionEffectImage, m_pixmap.rect()); 1712 return; 1713 } 1714 } 1715 1716 // ############################################################################## 1717 // ################## Serialization/Deserialization ########################### 1718 // ############################################################################## 1719 //! Save as XML 1720 void BoxPlot::save(QXmlStreamWriter* writer) const { 1721 Q_D(const BoxPlot); 1722 1723 writer->writeStartElement(QStringLiteral("boxPlot")); 1724 writeBasicAttributes(writer); 1725 writeCommentElement(writer); 1726 1727 // general 1728 writer->writeStartElement(QStringLiteral("general")); 1729 writer->writeAttribute(QStringLiteral("ordering"), QString::number(static_cast<int>(d->ordering))); 1730 writer->writeAttribute(QStringLiteral("orientation"), QString::number(static_cast<int>(d->orientation))); 1731 writer->writeAttribute(QStringLiteral("variableWidth"), QString::number(d->variableWidth)); 1732 writer->writeAttribute(QStringLiteral("widthFactor"), QString::number(d->widthFactor)); 1733 writer->writeAttribute(QStringLiteral("notches"), QString::number(d->notchesEnabled)); 1734 writer->writeAttribute(QStringLiteral("jitteringEnabled"), QString::number(d->jitteringEnabled)); 1735 writer->writeAttribute(QStringLiteral("plotRangeIndex"), QString::number(m_cSystemIndex)); 1736 writer->writeAttribute(QStringLiteral("xMin"), QString::number(d->xMin)); 1737 writer->writeAttribute(QStringLiteral("xMax"), QString::number(d->xMax)); 1738 writer->writeAttribute(QStringLiteral("yMin"), QString::number(d->yMin)); 1739 writer->writeAttribute(QStringLiteral("yMax"), QString::number(d->yMax)); 1740 writer->writeAttribute(QStringLiteral("legendVisible"), QString::number(d->legendVisible)); 1741 writer->writeAttribute(QStringLiteral("visible"), QString::number(d->isVisible())); 1742 for (auto* column : d->dataColumns) { 1743 writer->writeStartElement(QStringLiteral("column")); 1744 writer->writeAttribute(QStringLiteral("path"), column->path()); 1745 writer->writeEndElement(); 1746 } 1747 writer->writeEndElement(); 1748 1749 // box 1750 for (auto* background : d->backgrounds) 1751 background->save(writer); 1752 1753 for (auto* line : d->borderLines) 1754 line->save(writer); 1755 1756 for (auto* line : d->medianLines) 1757 line->save(writer); 1758 1759 // symbols for the outliers, mean, far out and jitter values 1760 d->symbolMean->save(writer); 1761 d->symbolMedian->save(writer); 1762 d->symbolOutlier->save(writer); 1763 d->symbolFarOut->save(writer); 1764 d->symbolData->save(writer); 1765 d->symbolWhiskerEnd->save(writer); 1766 1767 // whiskers 1768 writer->writeStartElement(QStringLiteral("whiskers")); 1769 writer->writeAttribute(QStringLiteral("type"), QString::number(static_cast<int>(d->whiskersType))); 1770 writer->writeAttribute(QStringLiteral("rangeParameter"), QString::number(d->whiskersRangeParameter)); 1771 d->whiskersLine->save(writer); 1772 writer->writeEndElement(); 1773 1774 writer->writeStartElement(QStringLiteral("whiskersCap")); 1775 writer->writeAttribute(QStringLiteral("size"), QString::number(d->whiskersCapSize)); 1776 d->whiskersCapLine->save(writer); 1777 writer->writeEndElement(); 1778 1779 // margin plots 1780 writer->writeStartElement(QStringLiteral("margins")); 1781 writer->writeAttribute(QStringLiteral("rugEnabled"), QString::number(d->rugEnabled)); 1782 writer->writeAttribute(QStringLiteral("rugLength"), QString::number(d->rugLength)); 1783 writer->writeAttribute(QStringLiteral("rugWidth"), QString::number(d->rugWidth)); 1784 writer->writeAttribute(QStringLiteral("rugOffset"), QString::number(d->rugOffset)); 1785 writer->writeEndElement(); 1786 1787 writer->writeEndElement(); // close "BoxPlot" section 1788 } 1789 1790 //! Load from XML 1791 bool BoxPlot::load(XmlStreamReader* reader, bool preview) { 1792 Q_D(BoxPlot); 1793 1794 if (!readBasicAttributes(reader)) 1795 return false; 1796 1797 QXmlStreamAttributes attribs; 1798 QString str; 1799 bool firstBackgroundRead = false; 1800 bool firstBorderLineRead = false; 1801 bool firstMedianLineRead = false; 1802 1803 while (!reader->atEnd()) { 1804 reader->readNext(); 1805 if (reader->isEndElement() && reader->name() == QLatin1String("boxPlot")) 1806 break; 1807 1808 if (!reader->isStartElement()) 1809 continue; 1810 1811 if (!preview && reader->name() == QLatin1String("comment")) { 1812 if (!readCommentElement(reader)) 1813 return false; 1814 } else if (!preview && reader->name() == QLatin1String("general")) { 1815 attribs = reader->attributes(); 1816 1817 READ_INT_VALUE("ordering", ordering, BoxPlot::Ordering); 1818 READ_INT_VALUE("orientation", orientation, BoxPlot::Orientation); 1819 READ_INT_VALUE("variableWidth", variableWidth, bool); 1820 READ_DOUBLE_VALUE("widthFactor", widthFactor); 1821 READ_INT_VALUE("notches", notchesEnabled, bool); 1822 READ_INT_VALUE("jitteringEnabled", jitteringEnabled, bool); 1823 READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int); 1824 1825 READ_DOUBLE_VALUE("xMin", xMin); 1826 READ_DOUBLE_VALUE("xMax", xMax); 1827 READ_DOUBLE_VALUE("yMin", yMin); 1828 READ_DOUBLE_VALUE("yMax", yMax); 1829 READ_INT_VALUE("legendVisible", legendVisible, bool); 1830 1831 str = attribs.value(QStringLiteral("visible")).toString(); 1832 if (str.isEmpty()) 1833 reader->raiseMissingAttributeWarning(QStringLiteral("visible")); 1834 else 1835 d->setVisible(str.toInt()); 1836 } else if (reader->name() == QLatin1String("column")) { 1837 attribs = reader->attributes(); 1838 1839 str = attribs.value(QStringLiteral("path")).toString(); 1840 if (!str.isEmpty()) 1841 d->dataColumnPaths << str; 1842 // READ_COLUMN(dataColumn); 1843 } else if (!preview && reader->name() == QLatin1String("filling")) 1844 if (!firstBackgroundRead) { 1845 auto* background = d->backgrounds.at(0); 1846 background->load(reader, preview); 1847 firstBackgroundRead = true; 1848 } else { 1849 auto* background = d->addBackground(KConfigGroup()); 1850 background->load(reader, preview); 1851 } 1852 else if (!preview && reader->name() == QLatin1String("border")) { 1853 if (!firstBorderLineRead) { 1854 auto* line = d->borderLines.at(0); 1855 line->load(reader, preview); 1856 firstBorderLineRead = true; 1857 } else { 1858 auto* line = d->addBorderLine(KConfigGroup()); 1859 line->load(reader, preview); 1860 } 1861 } else if (!preview && reader->name() == QLatin1String("medianLine")) { 1862 if (!firstMedianLineRead) { 1863 auto* line = d->medianLines.at(0); 1864 line->load(reader, preview); 1865 firstMedianLineRead = true; 1866 } else { 1867 auto* line = d->addMedianLine(KConfigGroup()); 1868 line->load(reader, preview); 1869 } 1870 } else if (!preview && reader->name() == QLatin1String("symbolMean")) 1871 d->symbolMean->load(reader, preview); 1872 else if (!preview && reader->name() == QLatin1String("symbolMedian")) 1873 d->symbolMedian->load(reader, preview); 1874 else if (!preview && reader->name() == QLatin1String("symbolOutlier")) 1875 d->symbolOutlier->load(reader, preview); 1876 else if (!preview && reader->name() == QLatin1String("symbolFarOut")) 1877 d->symbolFarOut->load(reader, preview); 1878 else if (!preview && reader->name() == QLatin1String("symbolData")) 1879 d->symbolData->load(reader, preview); 1880 else if (!preview && reader->name() == QLatin1String("symbolWhiskerEnd")) 1881 d->symbolWhiskerEnd->load(reader, preview); 1882 else if (!preview && reader->name() == QLatin1String("whiskers")) { 1883 attribs = reader->attributes(); 1884 1885 READ_INT_VALUE("type", whiskersType, BoxPlot::WhiskersType); 1886 READ_DOUBLE_VALUE("rangeParameter", whiskersRangeParameter); 1887 d->whiskersLine->load(reader, preview); 1888 } else if (!preview && reader->name() == QLatin1String("whiskersCap")) { 1889 attribs = reader->attributes(); 1890 1891 READ_DOUBLE_VALUE("size", whiskersCapSize); 1892 d->whiskersCapLine->load(reader, preview); 1893 } else if (!preview && reader->name() == QLatin1String("margins")) { 1894 attribs = reader->attributes(); 1895 1896 READ_INT_VALUE("rugEnabled", rugEnabled, bool); 1897 READ_DOUBLE_VALUE("rugLength", rugLength); 1898 READ_DOUBLE_VALUE("rugWidth", rugWidth); 1899 READ_DOUBLE_VALUE("rugOffset", rugOffset); 1900 } else { // unknown element 1901 reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); 1902 if (!reader->skipToEndElement()) 1903 return false; 1904 } 1905 } 1906 1907 d->dataColumns.resize(d->dataColumnPaths.size()); 1908 1909 // in case we're loading an older project where it was not possible to change the properties 1910 // for each data column independently of each other and there was only one single Background, etc. 1911 // add here additional elements to fit the current number of data columns after the project load 1912 // and set the saved properties for all newly added objects. 1913 int diff = d->dataColumns.size() - d->backgrounds.size(); 1914 if (diff > 0) { 1915 KConfig config; 1916 KConfigGroup group = config.group(QStringLiteral("XYCurve")); 1917 1918 const auto* background = d->backgrounds.constFirst(); 1919 const auto* borderLine = d->borderLines.constFirst(); 1920 const auto* medianLine = d->medianLines.constFirst(); 1921 for (int i = 0; i < diff; ++i) { 1922 auto* newBackground = d->addBackground(group); 1923 newBackground->setEnabled(background->enabled()); 1924 newBackground->setType(background->type()); 1925 newBackground->setColorStyle(background->colorStyle()); 1926 newBackground->setImageStyle(background->imageStyle()); 1927 newBackground->setBrushStyle(background->brushStyle()); 1928 newBackground->setFirstColor(background->firstColor()); 1929 newBackground->setSecondColor(background->secondColor()); 1930 newBackground->setFileName(background->fileName()); 1931 newBackground->setOpacity(background->opacity()); 1932 1933 auto* newBorderLine = d->addBorderLine(group); 1934 newBorderLine->setStyle(borderLine->style()); 1935 newBorderLine->setColor(borderLine->color()); 1936 newBorderLine->setWidth(borderLine->width()); 1937 newBorderLine->setOpacity(borderLine->opacity()); 1938 1939 auto* newMedianLine = d->addMedianLine(group); 1940 newMedianLine->setStyle(medianLine->style()); 1941 newMedianLine->setColor(medianLine->color()); 1942 newMedianLine->setWidth(medianLine->width()); 1943 newMedianLine->setOpacity(medianLine->opacity()); 1944 } 1945 } 1946 1947 return true; 1948 } 1949 1950 // ############################################################################## 1951 // ######################### Theme management ################################## 1952 // ############################################################################## 1953 void BoxPlot::loadThemeConfig(const KConfig& config) { 1954 KConfigGroup group; 1955 if (config.hasGroup(QStringLiteral("Theme"))) 1956 group = config.group(QStringLiteral("XYCurve")); // when loading from the theme config, use the same properties as for XYCurve 1957 else 1958 group = config.group(QStringLiteral("BoxPlot")); 1959 1960 const auto* plot = static_cast<const CartesianPlot*>(parentAspect()); 1961 int index = plot->curveChildIndex(this); 1962 const QColor themeColor = plot->themeColorPalette(index); 1963 1964 Q_D(BoxPlot); 1965 d->suppressRecalc = true; 1966 1967 // box fillings 1968 for (int i = 0; i < d->backgrounds.count(); ++i) { 1969 auto* background = d->backgrounds.at(i); 1970 background->loadThemeConfig(group, plot->themeColorPalette(i)); 1971 } 1972 1973 // box border lines 1974 for (int i = 0; i < d->borderLines.count(); ++i) { 1975 auto* line = d->borderLines.at(i); 1976 line->loadThemeConfig(group, plot->themeColorPalette(i)); 1977 } 1978 1979 // median lines 1980 for (int i = 0; i < d->medianLines.count(); ++i) { 1981 auto* line = d->medianLines.at(i); 1982 line->loadThemeConfig(group, plot->themeColorPalette(i)); 1983 } 1984 1985 // whiskers 1986 d->whiskersLine->loadThemeConfig(group, themeColor); 1987 d->whiskersCapLine->loadThemeConfig(group, themeColor); 1988 1989 // symbols 1990 d->symbolMean->loadThemeConfig(group, themeColor); 1991 d->symbolMedian->loadThemeConfig(group, themeColor); 1992 d->symbolOutlier->loadThemeConfig(group, themeColor); 1993 d->symbolFarOut->loadThemeConfig(group, themeColor); 1994 d->symbolData->loadThemeConfig(group, themeColor); 1995 1996 // Tufte's theme goes beyond what we can implement when using the theme properties of XYCurve. 1997 // So, instead of introducing a dedicated section for BoxPlot, which would be a big overkill 1998 // for all other themes, we add here a special handling for "Tufte". 1999 if (plot->theme() == QLatin1String("Tufte")) { 2000 for (auto* background : d->backgrounds) 2001 background->setEnabled(false); 2002 2003 for (auto* line : d->borderLines) 2004 line->setStyle(Qt::NoPen); 2005 2006 for (auto* line : d->medianLines) 2007 line->setStyle(Qt::NoPen); 2008 2009 d->symbolMean->setStyle(Symbol::Style::NoSymbols); 2010 d->symbolMedian->setStyle(Symbol::Style::Circle); 2011 d->symbolOutlier->setStyle(Symbol::Style::NoSymbols); 2012 d->symbolFarOut->setStyle(Symbol::Style::NoSymbols); 2013 d->symbolData->setStyle(Symbol::Style::NoSymbols); 2014 setWhiskersCapSize(0.0); 2015 } 2016 2017 d->suppressRecalc = false; 2018 d->recalcShapeAndBoundingRect(); 2019 }