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 }