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 }