File indexing completed on 2025-01-26 03:34:13

0001 /*
0002     File                 : LollipopPlot.cpp
0003     Project              : LabPlot
0004     Description          : Lollipop Plot
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2023 Alexander Semke <alexander.semke@web.de>
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "LollipopPlot.h"
0011 #include "LollipopPlotPrivate.h"
0012 #include "backend/core/Settings.h"
0013 #include "backend/core/column/Column.h"
0014 #include "backend/lib/XmlStreamReader.h"
0015 #include "backend/lib/commandtemplates.h"
0016 #include "backend/lib/trace.h"
0017 #include "backend/worksheet/Line.h"
0018 #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h"
0019 #include "backend/worksheet/plots/cartesian/CartesianPlot.h"
0020 #include "backend/worksheet/plots/cartesian/Symbol.h"
0021 #include "backend/worksheet/plots/cartesian/Value.h"
0022 #include "kdefrontend/GuiTools.h"
0023 #include "tools/ImageTools.h"
0024 
0025 #include <KConfig>
0026 #include <KConfigGroup>
0027 #include <KLocalizedString>
0028 
0029 #include <QActionGroup>
0030 #include <QGraphicsSceneMouseEvent>
0031 #include <QMenu>
0032 #include <QPainter>
0033 
0034 /**
0035  * \class LollipopPlot
0036  * \brief Lollipop Plot
0037  */
0038 
0039 LollipopPlot::LollipopPlot(const QString& name)
0040     : Plot(name, new LollipopPlotPrivate(this), AspectType::LollipopPlot) {
0041     init();
0042 }
0043 
0044 LollipopPlot::LollipopPlot(const QString& name, LollipopPlotPrivate* dd)
0045     : Plot(name, dd, AspectType::LollipopPlot) {
0046     init();
0047 }
0048 
0049 // no need to delete the d-pointer here - it inherits from QGraphicsItem
0050 // and is deleted during the cleanup in QGraphicsScene
0051 LollipopPlot::~LollipopPlot() = default;
0052 
0053 void LollipopPlot::init() {
0054     Q_D(LollipopPlot);
0055 
0056     KConfig config;
0057     const auto& group = config.group(QStringLiteral("LollipopPlot"));
0058 
0059     // general
0060     d->orientation = (LollipopPlot::Orientation)group.readEntry(QStringLiteral("Orientation"), (int)LollipopPlot::Orientation::Vertical);
0061 
0062     // initial line, symbol and value objects that will be available even if not data column was set yet
0063     d->addLine(group);
0064     d->addSymbol(group);
0065     d->addValue(group);
0066 }
0067 
0068 /*!
0069     Returns an icon to be used in the project explorer.
0070 */
0071 QIcon LollipopPlot::icon() const {
0072     return LollipopPlot::staticIcon();
0073 }
0074 
0075 QIcon LollipopPlot::staticIcon() {
0076     QPainter pa;
0077     pa.setRenderHint(QPainter::Antialiasing);
0078     int iconSize = 20;
0079     QPixmap pm(iconSize, iconSize);
0080 
0081     QPen pen(Qt::SolidLine);
0082     pen.setColor(GuiTools::isDarkMode() ? Qt::white : Qt::black);
0083     pen.setWidthF(0.0);
0084 
0085     pm.fill(Qt::transparent);
0086     pa.begin(&pm);
0087     pa.setPen(pen);
0088     pa.setBrush(pen.color());
0089     pa.drawLine(10, 6, 10, 14);
0090     pa.drawEllipse(8, 4, 4, 4);
0091     pa.end();
0092 
0093     return {pm};
0094 }
0095 
0096 void LollipopPlot::initActions() {
0097     // Orientation
0098     auto* orientationActionGroup = new QActionGroup(this);
0099     orientationActionGroup->setExclusive(true);
0100     connect(orientationActionGroup, &QActionGroup::triggered, this, &LollipopPlot::orientationChangedSlot);
0101 
0102     orientationHorizontalAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-move-horizontal")), i18n("Horizontal"), orientationActionGroup);
0103     orientationHorizontalAction->setCheckable(true);
0104 
0105     orientationVerticalAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-move-vertical")), i18n("Vertical"), orientationActionGroup);
0106     orientationVerticalAction->setCheckable(true);
0107 }
0108 
0109 void LollipopPlot::initMenus() {
0110     this->initActions();
0111 
0112     // Orientation
0113     orientationMenu = new QMenu(i18n("Orientation"));
0114     orientationMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-cross")));
0115     orientationMenu->addAction(orientationHorizontalAction);
0116     orientationMenu->addAction(orientationVerticalAction);
0117 }
0118 
0119 QMenu* LollipopPlot::createContextMenu() {
0120     if (!orientationMenu)
0121         initMenus();
0122 
0123     QMenu* menu = WorksheetElement::createContextMenu();
0124     QAction* visibilityAction = this->visibilityAction();
0125 
0126     // Orientation
0127     Q_D(const LollipopPlot);
0128     if (d->orientation == Orientation::Horizontal)
0129         orientationHorizontalAction->setChecked(true);
0130     else
0131         orientationVerticalAction->setChecked(true);
0132     menu->insertMenu(visibilityAction, orientationMenu);
0133     menu->insertSeparator(visibilityAction);
0134 
0135     return menu;
0136 }
0137 
0138 void LollipopPlot::retransform() {
0139     Q_D(LollipopPlot);
0140     d->retransform();
0141 }
0142 
0143 void LollipopPlot::recalc() {
0144     Q_D(LollipopPlot);
0145     d->recalc();
0146 }
0147 
0148 void LollipopPlot::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
0149 }
0150 
0151 /* ============================ getter methods ================= */
0152 // general
0153 BASIC_SHARED_D_READER_IMPL(LollipopPlot, QVector<const AbstractColumn*>, dataColumns, dataColumns)
0154 BASIC_SHARED_D_READER_IMPL(LollipopPlot, LollipopPlot::Orientation, orientation, orientation)
0155 BASIC_SHARED_D_READER_IMPL(LollipopPlot, const AbstractColumn*, xColumn, xColumn)
0156 
0157 QString& LollipopPlot::xColumnPath() const {
0158     D(LollipopPlot);
0159     return d->xColumnPath;
0160 }
0161 
0162 Line* LollipopPlot::lineAt(int index) const {
0163     Q_D(const LollipopPlot);
0164     if (index < d->lines.size())
0165         return d->lines.at(index);
0166     else
0167         return nullptr;
0168 }
0169 
0170 Symbol* LollipopPlot::symbolAt(int index) const {
0171     Q_D(const LollipopPlot);
0172     if (index < d->symbols.size())
0173         return d->symbols.at(index);
0174     else
0175         return nullptr;
0176 }
0177 
0178 QVector<QString>& LollipopPlot::dataColumnPaths() const {
0179     D(LollipopPlot);
0180     return d->dataColumnPaths;
0181 }
0182 
0183 double LollipopPlot::minimum(const Dimension dim) const {
0184     Q_D(const LollipopPlot);
0185     switch (dim) {
0186     case Dimension::X:
0187         return d->xMin;
0188     case Dimension::Y:
0189         return d->yMin;
0190     }
0191     return NAN;
0192 }
0193 
0194 double LollipopPlot::maximum(const Dimension dim) const {
0195     Q_D(const LollipopPlot);
0196     switch (dim) {
0197     case Dimension::X:
0198         return d->xMax;
0199     case Dimension::Y:
0200         return d->yMax;
0201     }
0202     return NAN;
0203 }
0204 
0205 bool LollipopPlot::hasData() const {
0206     Q_D(const LollipopPlot);
0207     return !d->dataColumns.isEmpty();
0208 }
0209 
0210 bool LollipopPlot::usingColumn(const Column* column) const {
0211     Q_D(const LollipopPlot);
0212 
0213     if (d->xColumn == column)
0214         return true;
0215 
0216     for (auto* c : d->dataColumns) {
0217         if (c == column)
0218             return true;
0219     }
0220 
0221     return false;
0222 }
0223 
0224 void LollipopPlot::updateColumnDependencies(const AbstractColumn* column) {
0225     Q_D(const LollipopPlot);
0226     const QString& columnPath = column->path();
0227     const auto dataColumnPaths = d->dataColumnPaths;
0228     auto dataColumns = d->dataColumns;
0229     bool changed = false;
0230 
0231     for (int i = 0; i < dataColumnPaths.count(); ++i) {
0232         const auto& path = dataColumnPaths.at(i);
0233 
0234         if (path == columnPath) {
0235             dataColumns[i] = column;
0236             changed = true;
0237         }
0238     }
0239 
0240     if (changed) {
0241         setUndoAware(false);
0242         setDataColumns(dataColumns);
0243         setUndoAware(true);
0244     }
0245 }
0246 
0247 QColor LollipopPlot::color() const {
0248     Q_D(const LollipopPlot);
0249     if (d->lines.size() > 0 && d->lines.at(0)->style() != Qt::PenStyle::NoPen)
0250         return d->lines.at(0)->pen().color();
0251     else if (d->symbols.size() > 0 && d->symbols.at(0)->style() != Symbol::Style::NoSymbols)
0252         return d->symbols.at(0)->pen().color();
0253     return QColor();
0254 }
0255 
0256 // values
0257 Value* LollipopPlot::value() const {
0258     Q_D(const LollipopPlot);
0259     return d->value;
0260 }
0261 
0262 /* ============================ setter methods and undo commands ================= */
0263 
0264 // General
0265 STD_SETTER_CMD_IMPL_F_S(LollipopPlot, SetXColumn, const AbstractColumn*, xColumn, recalc)
0266 void LollipopPlot::setXColumn(const AbstractColumn* column) {
0267     Q_D(LollipopPlot);
0268     if (column != d->xColumn) {
0269         exec(new LollipopPlotSetXColumnCmd(d, column, ki18n("%1: set x column")));
0270 
0271         if (column) {
0272             // update the curve itself on changes
0273             connect(column, &AbstractColumn::dataChanged, this, &LollipopPlot::recalc);
0274             if (column->parentAspect())
0275                 connect(column->parentAspect(), &AbstractAspect::childAspectAboutToBeRemoved, this, &LollipopPlot::dataColumnAboutToBeRemoved);
0276 
0277             connect(column, &AbstractColumn::dataChanged, this, &LollipopPlot::dataChanged);
0278             // TODO: add disconnect in the undo-function
0279         }
0280     }
0281 }
0282 
0283 STD_SETTER_CMD_IMPL_F_S(LollipopPlot, SetDataColumns, QVector<const AbstractColumn*>, dataColumns, recalc)
0284 void LollipopPlot::setDataColumns(const QVector<const AbstractColumn*> columns) {
0285     Q_D(LollipopPlot);
0286     if (columns != d->dataColumns) {
0287         exec(new LollipopPlotSetDataColumnsCmd(d, columns, ki18n("%1: set data columns")));
0288 
0289         for (auto* column : columns) {
0290             if (!column)
0291                 continue;
0292 
0293             // update the curve itself on changes
0294             connect(column, &AbstractColumn::dataChanged, this, &LollipopPlot::recalc);
0295             if (column->parentAspect())
0296                 connect(column->parentAspect(), &AbstractAspect::childAspectAboutToBeRemoved, this, &LollipopPlot::dataColumnAboutToBeRemoved);
0297             // TODO: add disconnect in the undo-function
0298 
0299             connect(column, &AbstractColumn::dataChanged, this, &LollipopPlot::dataChanged);
0300             connect(column, &AbstractAspect::aspectDescriptionChanged, this, &Plot::appearanceChanged);
0301         }
0302     }
0303 }
0304 
0305 STD_SETTER_CMD_IMPL_F_S(LollipopPlot, SetOrientation, LollipopPlot::Orientation, orientation, recalc)
0306 void LollipopPlot::setOrientation(LollipopPlot::Orientation orientation) {
0307     Q_D(LollipopPlot);
0308     if (orientation != d->orientation)
0309         exec(new LollipopPlotSetOrientationCmd(d, orientation, ki18n("%1: set orientation")));
0310 }
0311 
0312 // ##############################################################################
0313 // #################################  SLOTS  ####################################
0314 // ##############################################################################
0315 
0316 void LollipopPlot::dataColumnAboutToBeRemoved(const AbstractAspect* aspect) {
0317     Q_D(LollipopPlot);
0318     for (int i = 0; i < d->dataColumns.size(); ++i) {
0319         if (aspect == d->dataColumns.at(i)) {
0320             d->dataColumns[i] = nullptr;
0321             d->retransform();
0322             break;
0323         }
0324     }
0325 }
0326 
0327 // ##############################################################################
0328 // ######  SLOTs for changes triggered via QActions in the context menu  ########
0329 // ##############################################################################
0330 void LollipopPlot::orientationChangedSlot(QAction* action) {
0331     if (action == orientationHorizontalAction)
0332         this->setOrientation(Axis::Orientation::Horizontal);
0333     else
0334         this->setOrientation(Axis::Orientation::Vertical);
0335 }
0336 
0337 // ##############################################################################
0338 // ####################### Private implementation ###############################
0339 // ##############################################################################
0340 LollipopPlotPrivate::LollipopPlotPrivate(LollipopPlot* owner)
0341     : PlotPrivate(owner)
0342     , q(owner) {
0343     setFlag(QGraphicsItem::ItemIsSelectable);
0344     setAcceptHoverEvents(false);
0345 }
0346 
0347 Line* LollipopPlotPrivate::addLine(const KConfigGroup& group) {
0348     auto* line = new Line(QString());
0349     line->setHidden(true);
0350     q->addChild(line);
0351     if (!q->isLoading())
0352         line->init(group);
0353 
0354     q->connect(line, &Line::updatePixmapRequested, [=] {
0355         updatePixmap();
0356         Q_EMIT q->appearanceChanged();
0357     });
0358 
0359     q->connect(line, &Line::updateRequested, [=] {
0360         recalcShapeAndBoundingRect();
0361         Q_EMIT q->appearanceChanged();
0362     });
0363 
0364     lines << line;
0365 
0366     return line;
0367 }
0368 
0369 Symbol* LollipopPlotPrivate::addSymbol(const KConfigGroup& group) {
0370     auto* symbol = new Symbol(QString());
0371     symbol->setHidden(true);
0372     q->addChild(symbol);
0373 
0374     if (!q->isLoading())
0375         symbol->init(group);
0376 
0377     q->connect(symbol, &Symbol::updateRequested, [=] {
0378         updatePixmap();
0379         Q_EMIT q->appearanceChanged();
0380     });
0381 
0382     symbols << symbol;
0383 
0384     return symbol;
0385 }
0386 
0387 void LollipopPlotPrivate::addValue(const KConfigGroup& group) {
0388     value = new Value(QString());
0389     q->addChild(value);
0390     value->setHidden(true);
0391     value->setcenterPositionAvailable(true);
0392     if (!q->isLoading())
0393         value->init(group);
0394 
0395     q->connect(value, &Value::updatePixmapRequested, [=] {
0396         updatePixmap();
0397     });
0398 
0399     q->connect(value, &Value::updateRequested, [=] {
0400         updateValues();
0401     });
0402 }
0403 
0404 /*!
0405   called when the size of the plot or its data ranges (manual changes, zooming, etc.) were changed.
0406   recalculates the position of the scene points to be drawn.
0407   triggers the update of lines, drop lines, symbols etc.
0408 */
0409 void LollipopPlotPrivate::retransform() {
0410     if (suppressRetransform || !isVisible() || q->isLoading())
0411         return;
0412 
0413     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0414 
0415     const int count = dataColumns.size();
0416     if (!count || m_barLines.size() != count) {
0417         // no columns or recalc() was not called yet, nothing to do
0418         recalcShapeAndBoundingRect();
0419         return;
0420     }
0421 
0422     m_valuesPointsLogical.clear();
0423 
0424     if (count) {
0425         if (orientation == LollipopPlot::Orientation::Vertical) {
0426             for (int i = 0; i < count; ++i) {
0427                 if (dataColumns.at(i))
0428                     verticalPlot(i);
0429             }
0430         } else {
0431             for (int i = 0; i < count; ++i) {
0432                 if (dataColumns.at(i))
0433                     horizontalPlot(i);
0434             }
0435         }
0436     }
0437 
0438     updateValues(); // this also calls recalcShapeAndBoundingRect()
0439 }
0440 
0441 /*!
0442  * called when the data columns or their values were changed
0443  * calculates the min and max values for x and y and calls dataChanged()
0444  * to trigger the retransform in the parent plot
0445  */
0446 void LollipopPlotPrivate::recalc() {
0447     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0448 
0449     const int newSize = dataColumns.size();
0450     // resize the internal containers
0451     m_barLines.clear();
0452     m_barLines.resize(newSize);
0453     m_symbolPoints.clear();
0454     m_symbolPoints.resize(newSize);
0455 
0456     const double xMinOld = xMin;
0457     const double xMaxOld = xMax;
0458     const double yMinOld = yMin;
0459     const double yMaxOld = yMax;
0460 
0461     // bar properties
0462     int diff = newSize - lines.size();
0463     if (diff > 0) {
0464         // one more bar needs to be added
0465         KConfig config;
0466         KConfigGroup group = config.group(QLatin1String("LollipopPlot"));
0467         const auto* plot = static_cast<const CartesianPlot*>(q->parentAspect());
0468 
0469         for (int i = 0; i < diff; ++i) {
0470             auto* line = addLine(group);
0471             auto* symbol = addSymbol(group);
0472 
0473             if (plot) {
0474                 const auto& themeColor = plot->themeColorPalette(lines.count() - 1);
0475                 line->setColor(themeColor);
0476                 symbol->setColor(themeColor);
0477             }
0478         }
0479     } else if (diff < 0) {
0480         // the last bar was deleted
0481         //      if (newSize != 0) {
0482         //          delete backgrounds.takeLast();
0483         //      }
0484     }
0485 
0486     // determine the number of bar groups that we need to draw.
0487     // this number is equal to the max number of non-empty
0488     // values in the provided datasets
0489     int barGroupsCount = 0;
0490     int columnIndex = 0;
0491     for (auto* column : qAsConst(dataColumns)) {
0492         int size = static_cast<const Column*>(column)->statistics().size;
0493         m_barLines[columnIndex].resize(size);
0494         m_symbolPoints[columnIndex].resize(size);
0495         if (size > barGroupsCount)
0496             barGroupsCount = size;
0497 
0498         ++columnIndex;
0499     }
0500 
0501     // if an x-column was provided and it has less values than the count determined
0502     // above, we limit the number of bars to the number of values in the x-column
0503     if (xColumn) {
0504         int size = static_cast<const Column*>(xColumn)->statistics().size;
0505         if (size < barGroupsCount)
0506             barGroupsCount = size;
0507     }
0508 
0509     // determine min and max values for x- and y-ranges.
0510     // the first group is placed between 0 and 1, the second one between 1 and 2, etc.
0511     if (orientation == LollipopPlot::Orientation::Vertical) {
0512         // min/max for x
0513         if (xColumn) {
0514             xMin = xColumn->minimum() - 0.5;
0515             xMax = xColumn->maximum() + 0.5;
0516         } else {
0517             xMin = 0.0;
0518             xMax = barGroupsCount;
0519         }
0520 
0521         // min/max for y
0522         yMin = 0;
0523         yMax = -INFINITY;
0524         for (auto* column : dataColumns) {
0525             double max = column->maximum();
0526             if (max > yMax)
0527                 yMax = max;
0528 
0529             double min = column->minimum();
0530             if (min < yMin)
0531                 yMin = min;
0532         }
0533 
0534         // if there are no negative values, we plot
0535         // in the positive y-direction only and we start at y=0
0536         if (yMin > 0)
0537             yMin = 0;
0538     } else { // horizontal
0539         // min/max for x
0540         xMin = 0;
0541         xMax = -INFINITY;
0542         for (auto* column : dataColumns) {
0543             double max = column->maximum();
0544             if (max > xMax)
0545                 xMax = max;
0546 
0547             double min = column->minimum();
0548             if (min < xMin)
0549                 xMin = min;
0550         }
0551 
0552         // if there are no negative values, we plot
0553         // in the positive x-direction only and we start at x=0
0554         if (xMin > 0)
0555             xMin = 0;
0556 
0557         // min/max for y
0558         if (xColumn) {
0559             yMin = xColumn->minimum() - 0.5;
0560             yMax = xColumn->maximum() + 0.5;
0561         } else {
0562             yMin = 0.0;
0563             yMax = barGroupsCount;
0564         }
0565     }
0566 
0567     // determine the width of a group and of the gaps around a group
0568     m_groupWidth = 1.0;
0569     if (xColumn && newSize != 0)
0570         m_groupWidth = (xColumn->maximum() - xColumn->minimum()) / newSize;
0571 
0572     m_groupGap = m_groupWidth * 0.1; // gap around a group - the gap between two neighbour groups is 2*m_groupGap
0573 
0574     // if the size of the plot has changed because of the actual
0575     // data changes or because of new boxplot settings, emit dataChanged()
0576     // in order to recalculate the data ranges in the parent plot area
0577     // and to retransform all its children.
0578     // Just call retransform() to update the plot only if the ranges didn't change.
0579     if (xMin != xMinOld || xMax != xMaxOld || yMin != yMinOld || yMax != yMaxOld)
0580         Q_EMIT q->dataChanged();
0581     else
0582         retransform();
0583 }
0584 
0585 void LollipopPlotPrivate::verticalPlot(int columnIndex) {
0586     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0587 
0588     const auto* column = static_cast<const Column*>(dataColumns.at(columnIndex));
0589     QVector<QLineF> barLines; // lines for all bars for one colum in scene coordinates
0590     QVector<QPointF> symbolPoints;
0591 
0592     const double barGap = m_groupWidth * 0.1; // gap between two bars within a group
0593     const int barCount = dataColumns.size(); // number of bars within a group
0594     const double width = (m_groupWidth - 2 * m_groupGap - (barCount - 1) * barGap) / barCount; // bar width
0595 
0596     int valueIndex = 0;
0597     for (int i = 0; i < column->rowCount(); ++i) {
0598         if (!column->isValid(i) || column->isMasked(i))
0599             continue;
0600 
0601         const double value = column->valueAt(i);
0602         double x;
0603 
0604         if (xColumn)
0605             x = xColumn->valueAt(i);
0606         else
0607             x = m_groupGap
0608                 + valueIndex * m_groupWidth; // translate to the beginning of the group - 1st group is placed between 0 and 1, 2nd between 1 and 2, etc.
0609 
0610         x += (width + barGap) * columnIndex; // translate to the beginning of the bar within the current group
0611 
0612         symbolPoints << QPointF(x + width / 2, value);
0613         m_valuesPointsLogical << QPointF(x + width / 2, value);
0614         barLines << QLineF(x + width / 2, 0, x + width / 2, value);
0615         ++valueIndex;
0616     }
0617 
0618     m_barLines[columnIndex] = q->cSystem->mapLogicalToScene(barLines);
0619     m_symbolPoints[columnIndex] = q->cSystem->mapLogicalToScene(symbolPoints);
0620 }
0621 
0622 void LollipopPlotPrivate::horizontalPlot(int columnIndex) {
0623     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0624 
0625     const auto* column = static_cast<const Column*>(dataColumns.at(columnIndex));
0626     QVector<QLineF> barLines; // lines for all bars for one colum in scene coordinates
0627     QVector<QPointF> symbolPoints;
0628 
0629     const double barGap = m_groupWidth * 0.1; // gap between two bars within a group
0630     const int barCount = dataColumns.size(); // number of bars within a group
0631     const double width = (m_groupWidth - 2 * m_groupGap - (barCount - 1) * barGap) / barCount; // bar width
0632 
0633     int valueIndex = 0;
0634     for (int i = 0; i < column->rowCount(); ++i) {
0635         if (!column->isValid(i) || column->isMasked(i))
0636             continue;
0637 
0638         const double value = column->valueAt(i);
0639         double y;
0640         if (xColumn)
0641             y = xColumn->valueAt(i);
0642         else
0643             y = m_groupGap + valueIndex * m_groupWidth; // translate to the beginning of the group
0644 
0645         y += (width + barGap) * columnIndex; // translate to the beginning of the bar within the current group
0646 
0647         symbolPoints << QPointF(value, y - width / 2);
0648         m_valuesPointsLogical << QPointF(value, y - width / 2);
0649         barLines << QLineF(0, y - width / 2, value, y - width / 2);
0650         ++valueIndex;
0651     }
0652 
0653     m_barLines[columnIndex] = q->cSystem->mapLogicalToScene(barLines);
0654     m_symbolPoints[columnIndex] = q->cSystem->mapLogicalToScene(symbolPoints);
0655 }
0656 
0657 void LollipopPlotPrivate::updateValues() {
0658     m_valuesPath = QPainterPath();
0659     m_valuesPoints.clear();
0660     m_valuesStrings.clear();
0661 
0662     if (value->type() == Value::NoValues) {
0663         recalcShapeAndBoundingRect();
0664         return;
0665     }
0666 
0667     // determine the value string for all points that are currently visible in the plot
0668     auto visiblePoints = std::vector<bool>(m_valuesPointsLogical.count(), false);
0669     Points pointsScene;
0670     q->cSystem->mapLogicalToScene(m_valuesPointsLogical, pointsScene, visiblePoints);
0671     const auto& prefix = value->prefix();
0672     const auto& suffix = value->suffix();
0673     if (value->type() == Value::BinEntries) {
0674         for (int i = 0; i < m_valuesPointsLogical.count(); ++i) {
0675             if (!visiblePoints[i])
0676                 continue;
0677 
0678             auto& point = m_valuesPointsLogical.at(i);
0679             if (orientation == LollipopPlot::Orientation::Vertical)
0680                 m_valuesStrings << prefix + QString::number(point.y()) + suffix;
0681             else
0682                 m_valuesStrings << prefix + QString::number(point.x()) + suffix;
0683         }
0684     } else if (value->type() == Value::CustomColumn) {
0685         const auto* valuesColumn = value->column();
0686         if (!valuesColumn) {
0687             recalcShapeAndBoundingRect();
0688             return;
0689         }
0690 
0691 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0692         const int endRow = std::min(m_valuesPointsLogical.size(), static_cast<qsizetype>(valuesColumn->rowCount()));
0693 #else
0694         const int endRow = std::min(m_valuesPointsLogical.size(), valuesColumn->rowCount());
0695 #endif
0696         const auto xColMode = valuesColumn->columnMode();
0697         for (int i = 0; i < endRow; ++i) {
0698             if (!valuesColumn->isValid(i) || valuesColumn->isMasked(i))
0699                 continue;
0700 
0701             switch (xColMode) {
0702             case AbstractColumn::ColumnMode::Double:
0703                 m_valuesStrings << prefix + QString::number(valuesColumn->valueAt(i), value->numericFormat(), value->precision()) + suffix;
0704                 break;
0705             case AbstractColumn::ColumnMode::Integer:
0706             case AbstractColumn::ColumnMode::BigInt:
0707                 m_valuesStrings << prefix + QString::number(valuesColumn->valueAt(i)) + suffix;
0708                 break;
0709             case AbstractColumn::ColumnMode::Text:
0710                 m_valuesStrings << prefix + valuesColumn->textAt(i) + suffix;
0711                 break;
0712             case AbstractColumn::ColumnMode::DateTime:
0713             case AbstractColumn::ColumnMode::Month:
0714             case AbstractColumn::ColumnMode::Day:
0715                 m_valuesStrings << prefix + valuesColumn->dateTimeAt(i).toString(value->dateTimeFormat()) + suffix;
0716                 break;
0717             }
0718         }
0719     }
0720 
0721     // Calculate the coordinates where to paint the value strings.
0722     // The coordinates depend on the actual size of the string.
0723     QFontMetrics fm(value->font());
0724     qreal w;
0725     const qreal h = fm.ascent();
0726     int offset = value->distance();
0727 
0728     switch (value->position()) {
0729     case Value::Above:
0730         for (int i = 0; i < m_valuesStrings.size(); i++) {
0731             w = fm.boundingRect(m_valuesStrings.at(i)).width();
0732             if (orientation == LollipopPlot::Orientation::Vertical)
0733                 m_valuesPoints << QPointF(pointsScene.at(i).x() - w / 2, pointsScene.at(i).y() - offset);
0734             else
0735                 m_valuesPoints << QPointF(pointsScene.at(i).x() + offset, pointsScene.at(i).y() - w / 2);
0736         }
0737         break;
0738     case Value::Under:
0739         for (int i = 0; i < m_valuesStrings.size(); i++) {
0740             w = fm.boundingRect(m_valuesStrings.at(i)).width();
0741             if (orientation == LollipopPlot::Orientation::Vertical)
0742                 m_valuesPoints << QPointF(pointsScene.at(i).x() - w / 2, pointsScene.at(i).y() + offset + h / 2);
0743             else
0744                 m_valuesPoints << QPointF(pointsScene.at(i).x() - offset - h / 2, pointsScene.at(i).y() - w / 2);
0745         }
0746         break;
0747     case Value::Left:
0748         for (int i = 0; i < m_valuesStrings.size(); i++) {
0749             w = fm.boundingRect(m_valuesStrings.at(i)).width();
0750             if (orientation == LollipopPlot::Orientation::Vertical)
0751                 m_valuesPoints << QPointF(pointsScene.at(i).x() - offset - w, pointsScene.at(i).y());
0752             else
0753                 m_valuesPoints << QPointF(pointsScene.at(i).x(), pointsScene.at(i).y() - offset - w);
0754         }
0755         break;
0756     case Value::Right:
0757         for (int i = 0; i < m_valuesStrings.size(); i++) {
0758             w = fm.boundingRect(m_valuesStrings.at(i)).width();
0759             if (orientation == LollipopPlot::Orientation::Vertical)
0760                 m_valuesPoints << QPointF(pointsScene.at(i).x() + offset, pointsScene.at(i).y());
0761             else
0762                 m_valuesPoints << QPointF(pointsScene.at(i).x(), pointsScene.at(i).y() + offset);
0763         }
0764         break;
0765     case Value::Center:
0766         QVector<qreal> listBarWidth;
0767         for (const auto& columnBarLines : m_barLines) // loop over the different data columns
0768             for (const auto& line : columnBarLines)
0769                 listBarWidth.append(line.length());
0770 
0771         for (int i = 0; i < m_valuesStrings.size(); i++) {
0772             w = fm.boundingRect(m_valuesStrings.at(i)).width();
0773             const auto& point = pointsScene.at(i);
0774             if (orientation == LollipopPlot::Orientation::Vertical)
0775                 m_valuesPoints << QPointF(point.x() - w / 2,
0776                                           point.y() + listBarWidth.at(i) / 2 + offset - Worksheet::convertToSceneUnits(1, Worksheet::Unit::Point));
0777             else
0778                 m_valuesPoints << QPointF(point.x() - listBarWidth.at(i) / 2 - offset + h / 2 - w / 2, point.y() + h / 2);
0779         }
0780         break;
0781     }
0782 
0783     QTransform trafo;
0784     QPainterPath path;
0785     const double angle = value->rotationAngle();
0786     for (int i = 0; i < m_valuesPoints.size(); i++) {
0787         path = QPainterPath();
0788         path.addText(QPoint(0, 0), value->font(), m_valuesStrings.at(i));
0789 
0790         trafo.reset();
0791         trafo.translate(m_valuesPoints.at(i).x(), m_valuesPoints.at(i).y());
0792         if (angle != 0.)
0793             trafo.rotate(-angle);
0794 
0795         m_valuesPath.addPath(trafo.map(path));
0796     }
0797 
0798     recalcShapeAndBoundingRect();
0799 }
0800 
0801 /*!
0802   recalculates the outer bounds and the shape of the item.
0803 */
0804 void LollipopPlotPrivate::recalcShapeAndBoundingRect() {
0805     prepareGeometryChange();
0806     m_shape = QPainterPath();
0807 
0808     // lines
0809     int index = 0;
0810     for (const auto& columnBarLines : m_barLines) { // loop over the different data columns
0811         for (const auto& line : columnBarLines) { // loop over the bars for every data column
0812             QPainterPath barPath;
0813             barPath.moveTo(line.p1());
0814             barPath.lineTo(line.p2());
0815 
0816             if (index < lines.count()) { // TODO
0817                 const auto& borderPen = lines.at(index)->pen();
0818                 m_shape.addPath(WorksheetElement::shapeFromPath(barPath, borderPen));
0819             }
0820         }
0821         ++index;
0822     }
0823 
0824     // symbols
0825     auto symbolsPath = QPainterPath();
0826     index = 0;
0827     for (const auto& symbolPoints : m_symbolPoints) { // loop over the different data columns
0828         if (index > symbols.count() - 1)
0829             continue;
0830 
0831         const auto* symbol = symbols.at(index);
0832         if (symbol->style() != Symbol::Style::NoSymbols) {
0833             auto path = Symbol::stylePath(symbol->style());
0834 
0835             QTransform trafo;
0836             trafo.scale(symbol->size(), symbol->size());
0837             path = trafo.map(path);
0838             trafo.reset();
0839 
0840             if (symbol->rotationAngle() != 0.) {
0841                 trafo.rotate(symbol->rotationAngle());
0842                 path = trafo.map(path);
0843             }
0844 
0845             for (const auto& point : symbolPoints) { // loop over the points for every data column
0846                 trafo.reset();
0847                 trafo.translate(point.x(), point.y());
0848                 symbolsPath.addPath(trafo.map(path));
0849             }
0850         }
0851         ++index;
0852     }
0853 
0854     m_shape.addPath(symbolsPath);
0855 
0856     if (value->type() != Value::NoValues)
0857         m_shape.addPath(m_valuesPath);
0858 
0859     m_boundingRectangle = m_shape.boundingRect();
0860     updatePixmap();
0861 }
0862 
0863 void LollipopPlotPrivate::updatePixmap() {
0864     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0865     QPixmap pixmap(m_boundingRectangle.width(), m_boundingRectangle.height());
0866     if (m_boundingRectangle.width() == 0. || m_boundingRectangle.height() == 0.) {
0867         m_pixmap = pixmap;
0868         m_hoverEffectImageIsDirty = true;
0869         m_selectionEffectImageIsDirty = true;
0870         return;
0871     }
0872     pixmap.fill(Qt::transparent);
0873     QPainter painter(&pixmap);
0874     painter.setRenderHint(QPainter::Antialiasing, true);
0875     painter.translate(-m_boundingRectangle.topLeft());
0876 
0877     draw(&painter);
0878     painter.end();
0879 
0880     m_pixmap = pixmap;
0881     m_hoverEffectImageIsDirty = true;
0882     m_selectionEffectImageIsDirty = true;
0883     Q_EMIT q->changed();
0884     update();
0885 }
0886 
0887 void LollipopPlotPrivate::draw(QPainter* painter) {
0888     PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
0889 
0890     int columnIndex = 0;
0891     for (const auto& columnBarLines : m_barLines) { // loop over the different data columns
0892         // draw the lines
0893         if (columnIndex < lines.size()) { // TODO: remove this check later
0894             const auto& borderPen = lines.at(columnIndex)->pen();
0895             const double borderOpacity = lines.at(columnIndex)->opacity();
0896             for (const auto& line : columnBarLines) { // loop over the bars for every data column
0897                 if (borderPen.style() != Qt::NoPen) {
0898                     painter->setPen(borderPen);
0899                     painter->setBrush(Qt::NoBrush);
0900                     painter->setOpacity(borderOpacity);
0901                     painter->drawLine(line);
0902                 }
0903             }
0904         }
0905 
0906         // draw symbols
0907         if (columnIndex < symbols.size())
0908             symbols.at(columnIndex)->draw(painter, m_symbolPoints.at(columnIndex));
0909 
0910         ++columnIndex;
0911     }
0912 
0913     // draw values
0914     value->draw(painter, m_valuesPoints, m_valuesStrings);
0915 }
0916 
0917 void LollipopPlotPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
0918     if (!isVisible())
0919         return;
0920 
0921     painter->setPen(Qt::NoPen);
0922     painter->setBrush(Qt::NoBrush);
0923     painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
0924 
0925     if (Settings::group(QStringLiteral("Settings_Worksheet")).readEntry<bool>("DoubleBuffering", true))
0926         painter->drawPixmap(m_boundingRectangle.topLeft(), m_pixmap); // draw the cached pixmap (fast)
0927     else
0928         draw(painter); // draw directly again (slow)
0929 
0930     if (m_hovered && !isSelected() && !q->isPrinting()) {
0931         if (m_hoverEffectImageIsDirty) {
0932             QPixmap pix = m_pixmap;
0933             QPainter p(&pix);
0934             p.setCompositionMode(QPainter::CompositionMode_SourceIn); // source (shadow) pixels merged with the alpha channel of the destination (m_pixmap)
0935             p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Shadow));
0936             p.end();
0937 
0938             m_hoverEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5);
0939             m_hoverEffectImageIsDirty = false;
0940         }
0941 
0942         painter->drawImage(m_boundingRectangle.topLeft(), m_hoverEffectImage, m_pixmap.rect());
0943         return;
0944     }
0945 
0946     if (isSelected() && !q->isPrinting()) {
0947         if (m_selectionEffectImageIsDirty) {
0948             QPixmap pix = m_pixmap;
0949             QPainter p(&pix);
0950             p.setCompositionMode(QPainter::CompositionMode_SourceIn);
0951             p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Highlight));
0952             p.end();
0953 
0954             m_selectionEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5);
0955             m_selectionEffectImageIsDirty = false;
0956         }
0957 
0958         painter->drawImage(m_boundingRectangle.topLeft(), m_selectionEffectImage, m_pixmap.rect());
0959         return;
0960     }
0961 }
0962 
0963 // ##############################################################################
0964 // ##################  Serialization/Deserialization  ###########################
0965 // ##############################################################################
0966 //! Save as XML
0967 void LollipopPlot::save(QXmlStreamWriter* writer) const {
0968     Q_D(const LollipopPlot);
0969 
0970     writer->writeStartElement(QStringLiteral("lollipopPlot"));
0971     writeBasicAttributes(writer);
0972     writeCommentElement(writer);
0973 
0974     // general
0975     writer->writeStartElement(QStringLiteral("general"));
0976     writer->writeAttribute(QStringLiteral("orientation"), QString::number(static_cast<int>(d->orientation)));
0977     writer->writeAttribute(QStringLiteral("plotRangeIndex"), QString::number(m_cSystemIndex));
0978     writer->writeAttribute(QStringLiteral("xMin"), QString::number(d->xMin));
0979     writer->writeAttribute(QStringLiteral("xMax"), QString::number(d->xMax));
0980     writer->writeAttribute(QStringLiteral("yMin"), QString::number(d->yMin));
0981     writer->writeAttribute(QStringLiteral("yMax"), QString::number(d->yMax));
0982     writer->writeAttribute(QStringLiteral("legendVisible"), QString::number(d->legendVisible));
0983     writer->writeAttribute(QStringLiteral("visible"), QString::number(d->isVisible()));
0984 
0985     if (d->xColumn)
0986         writer->writeAttribute(QStringLiteral("xColumn"), d->xColumn->path());
0987 
0988     for (auto* column : d->dataColumns) {
0989         writer->writeStartElement(QStringLiteral("column"));
0990         writer->writeAttribute(QStringLiteral("path"), column->path());
0991         writer->writeEndElement();
0992     }
0993     writer->writeEndElement();
0994 
0995     // lines
0996     for (auto* line : d->lines)
0997         line->save(writer);
0998 
0999     // symbols
1000     for (auto* symbol : d->symbols)
1001         symbol->save(writer);
1002 
1003     // Values
1004     d->value->save(writer);
1005 
1006     writer->writeEndElement(); // close "LollipopPlot" section
1007 }
1008 
1009 //! Load from XML
1010 bool LollipopPlot::load(XmlStreamReader* reader, bool preview) {
1011     Q_D(LollipopPlot);
1012 
1013     if (!readBasicAttributes(reader))
1014         return false;
1015 
1016     QXmlStreamAttributes attribs;
1017     QString str;
1018     bool firstLineRead = false;
1019     bool firstSymbolRead = false;
1020 
1021     while (!reader->atEnd()) {
1022         reader->readNext();
1023         if (reader->isEndElement() && reader->name() == QLatin1String("lollipopPlot"))
1024             break;
1025 
1026         if (!reader->isStartElement())
1027             continue;
1028 
1029         if (!preview && reader->name() == QLatin1String("comment")) {
1030             if (!readCommentElement(reader))
1031                 return false;
1032         } else if (!preview && reader->name() == QLatin1String("general")) {
1033             attribs = reader->attributes();
1034 
1035             READ_INT_VALUE("orientation", orientation, LollipopPlot::Orientation);
1036             READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int);
1037 
1038             READ_DOUBLE_VALUE("xMin", xMin);
1039             READ_DOUBLE_VALUE("xMax", xMax);
1040             READ_DOUBLE_VALUE("yMin", yMin);
1041             READ_DOUBLE_VALUE("yMax", yMax);
1042             READ_COLUMN(xColumn);
1043             READ_INT_VALUE("legendVisible", legendVisible, bool);
1044 
1045             str = attribs.value(QStringLiteral("visible")).toString();
1046             if (str.isEmpty())
1047                 reader->raiseMissingAttributeWarning(QStringLiteral("visible"));
1048             else
1049                 d->setVisible(str.toInt());
1050         } else if (reader->name() == QLatin1String("column")) {
1051             attribs = reader->attributes();
1052 
1053             str = attribs.value(QStringLiteral("path")).toString();
1054             if (!str.isEmpty())
1055                 d->dataColumnPaths << str;
1056             //          READ_COLUMN(dataColumn);
1057         } else if (!preview && reader->name() == QLatin1String("line")) {
1058             if (!firstLineRead) {
1059                 auto* line = d->lines.at(0);
1060                 line->load(reader, preview);
1061                 firstLineRead = true;
1062             } else {
1063                 auto* line = d->addLine(KConfigGroup());
1064                 line->load(reader, preview);
1065             }
1066         } else if (!preview && reader->name() == QLatin1String("symbol")) {
1067             if (!firstSymbolRead) {
1068                 auto* symbol = d->symbols.at(0);
1069                 symbol->load(reader, preview);
1070                 firstSymbolRead = true;
1071             } else {
1072                 auto* symbol = d->addSymbol(KConfigGroup());
1073                 symbol->load(reader, preview);
1074             }
1075         } else if (!preview && reader->name() == QLatin1String("values")) {
1076             d->value->load(reader, preview);
1077         } else { // unknown element
1078             reader->raiseUnknownElementWarning();
1079             if (!reader->skipToEndElement())
1080                 return false;
1081         }
1082     }
1083 
1084     d->dataColumns.resize(d->dataColumnPaths.size());
1085 
1086     return true;
1087 }
1088 
1089 // ##############################################################################
1090 // #########################  Theme management ##################################
1091 // ##############################################################################
1092 void LollipopPlot::loadThemeConfig(const KConfig& config) {
1093     KConfigGroup group;
1094     if (config.hasGroup(QStringLiteral("Theme")))
1095         group = config.group(QStringLiteral("XYCurve")); // when loading from the theme config, use the same properties as for XYCurve
1096     else
1097         group = config.group(QStringLiteral("LollipopPlot"));
1098 
1099     const auto* plot = static_cast<const CartesianPlot*>(parentAspect());
1100     int index = plot->curveChildIndex(this);
1101     const QColor themeColor = plot->themeColorPalette(index);
1102 
1103     Q_D(LollipopPlot);
1104     d->suppressRecalc = true;
1105 
1106     // lines
1107     for (int i = 0; i < d->lines.count(); ++i) {
1108         auto* line = d->lines.at(i);
1109         line->loadThemeConfig(group, plot->themeColorPalette(i));
1110     }
1111 
1112     // symbols
1113     for (int i = 0; i < d->symbols.count(); ++i) {
1114         auto* symbol = d->symbols.at(i);
1115         symbol->loadThemeConfig(group, plot->themeColorPalette(i));
1116     }
1117 
1118     // values
1119     d->value->loadThemeConfig(group, themeColor);
1120 
1121     d->suppressRecalc = false;
1122     d->recalcShapeAndBoundingRect();
1123 }