File indexing completed on 2025-01-26 03:34:17
0001 /* 0002 File : XYCurve.cpp 0003 Project : LabPlot 0004 Description : A xy-curve 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2010-2024 Alexander Semke <alexander.semke@web.de> 0007 SPDX-FileCopyrightText: 2013-2021 Stefan Gerlach <stefan.gerlach@uni.kn> 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 /*! 0012 \class XYCurve 0013 \brief A 2D-curve, provides an interface for editing many properties of the curve. 0014 0015 \ingroup worksheet 0016 */ 0017 0018 #include "XYCurve.h" 0019 #include "XYCurvePrivate.h" 0020 #include "backend/core/AbstractColumn.h" 0021 #include "backend/core/Project.h" 0022 #include "backend/core/Settings.h" 0023 #include "backend/core/column/Column.h" 0024 #include "backend/gsl/errors.h" 0025 #include "backend/lib/XmlStreamReader.h" 0026 #include "backend/lib/commandtemplates.h" 0027 #include "backend/lib/macrosCurve.h" 0028 #include "backend/lib/trace.h" 0029 #include "backend/spreadsheet/Spreadsheet.h" 0030 #include "backend/worksheet/Background.h" 0031 #include "backend/worksheet/Line.h" 0032 #include "backend/worksheet/Worksheet.h" 0033 #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h" 0034 #include "backend/worksheet/plots/cartesian/CartesianPlot.h" 0035 #include "backend/worksheet/plots/cartesian/ErrorBarStyle.h" 0036 #include "backend/worksheet/plots/cartesian/Symbol.h" 0037 #include "tools/ImageTools.h" 0038 0039 #include <KConfig> 0040 #include <KConfigGroup> 0041 #include <KLocalizedString> 0042 0043 #include <QGraphicsSceneMouseEvent> 0044 #include <QMenu> 0045 #include <QPainter> 0046 #include <QScreen> 0047 0048 #include <gsl/gsl_errno.h> 0049 #include <gsl/gsl_math.h> 0050 #include <gsl/gsl_spline.h> 0051 0052 using Dimension = CartesianCoordinateSystem::Dimension; 0053 0054 CURVE_COLUMN_CONNECT(XYCurve, X, x, recalcLogicalPoints) 0055 CURVE_COLUMN_CONNECT(XYCurve, Y, y, recalcLogicalPoints) 0056 CURVE_COLUMN_CONNECT(XYCurve, Values, values, recalcLogicalPoints) 0057 0058 XYCurve::XYCurve(const QString& name, AspectType type) 0059 : Plot(name, new XYCurvePrivate(this), type) { 0060 init(); 0061 } 0062 0063 XYCurve::XYCurve(const QString& name, XYCurvePrivate* dd, AspectType type) 0064 : Plot(name, dd, type) { 0065 init(); 0066 } 0067 0068 // no need to delete the d-pointer here - it inherits from QGraphicsItem 0069 // and is deleted during the cleanup in QGraphicsScene 0070 XYCurve::~XYCurve() = default; 0071 0072 void XYCurve::init() { 0073 Q_D(XYCurve); 0074 0075 KConfig config; 0076 KConfigGroup group = config.group(QStringLiteral("XYCurve")); 0077 0078 d->lineType = (LineType)group.readEntry(QStringLiteral("LineType"), static_cast<int>(LineType::Line)); 0079 d->lineIncreasingXOnly = group.readEntry(QStringLiteral("LineIncreasingXOnly"), false); 0080 d->lineSkipGaps = group.readEntry(QStringLiteral("SkipLineGaps"), false); 0081 d->lineInterpolationPointsCount = group.readEntry(QStringLiteral("LineInterpolationPointsCount"), 1); 0082 0083 d->line = new Line(QString()); 0084 d->line->setCreateXmlElement(false); 0085 d->line->setHidden(true); 0086 addChild(d->line); 0087 d->line->init(group); 0088 connect(d->line, &Line::updatePixmapRequested, [=] { 0089 d->updatePixmap(); 0090 Q_EMIT appearanceChanged(); 0091 }); 0092 connect(d->line, &Line::updateRequested, [=] { 0093 d->recalcShapeAndBoundingRect(); 0094 Q_EMIT appearanceChanged(); 0095 }); 0096 0097 d->dropLine = new Line(QString()); 0098 d->dropLine->setPrefix(QStringLiteral("DropLine")); 0099 d->dropLine->setHidden(true); 0100 addChild(d->dropLine); 0101 d->dropLine->init(group); 0102 connect(d->dropLine, &Line::dropLineTypeChanged, [=] { 0103 d->updateDropLines(); 0104 }); 0105 connect(d->dropLine, &Line::updatePixmapRequested, [=] { 0106 d->updatePixmap(); 0107 }); 0108 connect(d->dropLine, &Line::updateRequested, [=] { 0109 d->recalcShapeAndBoundingRect(); 0110 }); 0111 0112 // initialize the symbol 0113 d->symbol = new Symbol(QString()); 0114 addChild(d->symbol); 0115 d->symbol->setHidden(true); 0116 d->symbol->init(group); 0117 connect(d->symbol, &Symbol::updateRequested, [=] { 0118 d->updateSymbols(); 0119 Q_EMIT appearanceChanged(); 0120 }); 0121 connect(d->symbol, &Symbol::updatePixmapRequested, [=] { 0122 d->updatePixmap(); 0123 Q_EMIT appearanceChanged(); 0124 }); 0125 0126 // values 0127 d->valuesType = (ValuesType)group.readEntry(QStringLiteral("ValuesType"), static_cast<int>(ValuesType::NoValues)); 0128 d->valuesPosition = (ValuesPosition)group.readEntry(QStringLiteral("ValuesPosition"), static_cast<int>(ValuesPosition::Above)); 0129 d->valuesDistance = group.readEntry(QStringLiteral("ValuesDistance"), Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point)); 0130 d->valuesRotationAngle = group.readEntry(QStringLiteral("ValuesRotation"), 0.0); 0131 d->valuesOpacity = group.readEntry(QStringLiteral("ValuesOpacity"), 1.0); 0132 d->valuesNumericFormat = group.readEntry(QStringLiteral("ValuesNumericFormat"), QStringLiteral("f")).at(0).toLatin1(); 0133 d->valuesPrecision = group.readEntry(QStringLiteral("ValuesNumericFormat"), 2); 0134 d->valuesDateTimeFormat = group.readEntry(QStringLiteral("ValuesDateTimeFormat"), QStringLiteral("yyyy-MM-dd")); 0135 d->valuesPrefix = group.readEntry(QStringLiteral("ValuesPrefix"), QStringLiteral("")); 0136 d->valuesSuffix = group.readEntry(QStringLiteral("ValuesSuffix"), QStringLiteral("")); 0137 d->valuesFont = group.readEntry(QStringLiteral("ValuesFont"), QFont()); 0138 d->valuesFont.setPixelSize(Worksheet::convertToSceneUnits(8, Worksheet::Unit::Point)); 0139 d->valuesColor = group.readEntry(QStringLiteral("ValuesColor"), QColor(Qt::black)); 0140 0141 // Background/Filling 0142 d->background = new Background(QString()); 0143 d->background->setPrefix(QStringLiteral("Filling")); 0144 d->background->setPositionAvailable(true); 0145 addChild(d->background); 0146 d->background->setHidden(true); 0147 d->background->init(group); 0148 connect(d->background, &Background::updateRequested, [=] { 0149 d->updatePixmap(); 0150 }); 0151 connect(d->background, &Background::updatePositionRequested, [=] { 0152 d->updateFilling(); 0153 }); 0154 0155 // error bars 0156 d->xErrorBar = new ErrorBar(QString()); 0157 d->xErrorBar->setPrefix(QStringLiteral("X")); 0158 addChild(d->xErrorBar); 0159 d->xErrorBar->setHidden(true); 0160 d->xErrorBar->init(group); 0161 connect(d->xErrorBar, &ErrorBar::updateRequested, [=] { 0162 d->updateErrorBars(); 0163 }); 0164 0165 d->yErrorBar = new ErrorBar(QString()); 0166 d->yErrorBar->setPrefix(QStringLiteral("Y")); 0167 addChild(d->yErrorBar); 0168 d->yErrorBar->setHidden(true); 0169 d->yErrorBar->init(group); 0170 connect(d->yErrorBar, &ErrorBar::updateRequested, [=] { 0171 d->updateErrorBars(); 0172 }); 0173 0174 d->errorBarStyle = new ErrorBarStyle(QString()); 0175 addChild(d->errorBarStyle); 0176 d->errorBarStyle->setHidden(true); 0177 d->errorBarStyle->init(group); 0178 connect(d->errorBarStyle, &ErrorBarStyle::updateRequested, [=] { 0179 d->updateErrorBars(); 0180 }); 0181 connect(d->errorBarStyle, &ErrorBarStyle::updatePixmapRequested, [=] { 0182 d->updatePixmap(); 0183 }); 0184 0185 // marginal plots (rug, histogram, boxplot) 0186 d->rugEnabled = group.readEntry(QStringLiteral("RugEnabled"), false); 0187 d->rugOrientation = (WorksheetElement::Orientation)group.readEntry(QStringLiteral("RugOrientation"), (int)WorksheetElement::Orientation::Both); 0188 d->rugLength = group.readEntry(QStringLiteral("RugLength"), Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point)); 0189 d->rugWidth = group.readEntry(QStringLiteral("RugWidth"), 0.0); 0190 d->rugOffset = group.readEntry(QStringLiteral("RugOffset"), 0.0); 0191 } 0192 0193 void XYCurve::initActions() { 0194 navigateToAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next-view")), QString(), this); 0195 connect(navigateToAction, SIGNAL(triggered(bool)), this, SLOT(navigateTo())); 0196 0197 m_menusInitialized = true; 0198 } 0199 0200 QMenu* XYCurve::createContextMenu() { 0201 if (!m_menusInitialized) 0202 initActions(); 0203 0204 QMenu* menu = WorksheetElement::createContextMenu(); 0205 QAction* visibilityAction = this->visibilityAction(); // skip the first action because of the "title-action", second is visible 0206 0207 //"data analysis" menu 0208 // auto* plot = static_cast<CartesianPlot*>(parentAspect()); 0209 menu->insertMenu(visibilityAction, m_plot->analysisMenu()); 0210 menu->insertSeparator(visibilityAction); 0211 0212 //"Navigate to spreadsheet"-action, show only if x- or y-columns have data from a spreadsheet 0213 AbstractAspect* parentSpreadsheet = nullptr; 0214 if (xColumn() && dynamic_cast<Spreadsheet*>(xColumn()->parentAspect())) 0215 parentSpreadsheet = xColumn()->parentAspect(); 0216 else if (yColumn() && dynamic_cast<Spreadsheet*>(yColumn()->parentAspect())) 0217 parentSpreadsheet = yColumn()->parentAspect(); 0218 0219 if (parentSpreadsheet) { 0220 navigateToAction->setText(i18n("Navigate to \"%1\"", parentSpreadsheet->name())); 0221 navigateToAction->setData(parentSpreadsheet->path()); 0222 menu->insertAction(visibilityAction, navigateToAction); 0223 menu->insertSeparator(visibilityAction); 0224 } 0225 0226 // if the context menu is called on an item that is not selected yet, select it 0227 if (!graphicsItem()->isSelected()) 0228 graphicsItem()->setSelected(true); 0229 0230 return menu; 0231 } 0232 0233 /*! 0234 Returns an icon to be used in the project explorer. 0235 */ 0236 QIcon XYCurve::icon() const { 0237 return QIcon::fromTheme(QStringLiteral("labplot-xy-curve")); 0238 } 0239 0240 // ############################################################################## 0241 // ########################## getter methods ################################## 0242 // ############################################################################## 0243 // data source 0244 const AbstractColumn* XYCurve::column(const Dimension dim) const { 0245 switch (dim) { 0246 case Dimension::X: 0247 return xColumn(); 0248 case Dimension::Y: 0249 return yColumn(); 0250 } 0251 return nullptr; 0252 } 0253 BASIC_SHARED_D_READER_IMPL(XYCurve, const AbstractColumn*, xColumn, xColumn) 0254 BASIC_SHARED_D_READER_IMPL(XYCurve, const AbstractColumn*, yColumn, yColumn) 0255 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, xColumnPath, xColumnPath) 0256 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, yColumnPath, yColumnPath) 0257 0258 // line 0259 BASIC_SHARED_D_READER_IMPL(XYCurve, XYCurve::LineType, lineType, lineType) 0260 BASIC_SHARED_D_READER_IMPL(XYCurve, bool, lineSkipGaps, lineSkipGaps) 0261 BASIC_SHARED_D_READER_IMPL(XYCurve, bool, lineIncreasingXOnly, lineIncreasingXOnly) 0262 BASIC_SHARED_D_READER_IMPL(XYCurve, int, lineInterpolationPointsCount, lineInterpolationPointsCount) 0263 0264 Line* XYCurve::line() const { 0265 Q_D(const XYCurve); 0266 return d->line; 0267 } 0268 0269 Line* XYCurve::dropLine() const { 0270 Q_D(const XYCurve); 0271 return d->dropLine; 0272 } 0273 0274 // symbols 0275 Symbol* XYCurve::symbol() const { 0276 Q_D(const XYCurve); 0277 return d->symbol; 0278 } 0279 0280 // values 0281 BASIC_SHARED_D_READER_IMPL(XYCurve, XYCurve::ValuesType, valuesType, valuesType) 0282 BASIC_SHARED_D_READER_IMPL(XYCurve, const AbstractColumn*, valuesColumn, valuesColumn) 0283 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, valuesColumnPath, valuesColumnPath) 0284 0285 BASIC_SHARED_D_READER_IMPL(XYCurve, XYCurve::ValuesPosition, valuesPosition, valuesPosition) 0286 BASIC_SHARED_D_READER_IMPL(XYCurve, qreal, valuesDistance, valuesDistance) 0287 BASIC_SHARED_D_READER_IMPL(XYCurve, qreal, valuesRotationAngle, valuesRotationAngle) 0288 BASIC_SHARED_D_READER_IMPL(XYCurve, qreal, valuesOpacity, valuesOpacity) 0289 BASIC_SHARED_D_READER_IMPL(XYCurve, char, valuesNumericFormat, valuesNumericFormat) 0290 BASIC_SHARED_D_READER_IMPL(XYCurve, int, valuesPrecision, valuesPrecision) 0291 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, valuesDateTimeFormat, valuesDateTimeFormat) 0292 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, valuesPrefix, valuesPrefix) 0293 BASIC_SHARED_D_READER_IMPL(XYCurve, QString, valuesSuffix, valuesSuffix) 0294 BASIC_SHARED_D_READER_IMPL(XYCurve, QColor, valuesColor, valuesColor) 0295 BASIC_SHARED_D_READER_IMPL(XYCurve, QFont, valuesFont, valuesFont) 0296 0297 // Filling 0298 Background* XYCurve::background() const { 0299 Q_D(const XYCurve); 0300 return d->background; 0301 } 0302 0303 // error bars 0304 ErrorBar* XYCurve::xErrorBar() const { 0305 Q_D(const XYCurve); 0306 return d->xErrorBar; 0307 } 0308 0309 ErrorBar* XYCurve::yErrorBar() const { 0310 Q_D(const XYCurve); 0311 return d->yErrorBar; 0312 } 0313 0314 ErrorBarStyle* XYCurve::errorBarStyle() const { 0315 Q_D(const XYCurve); 0316 return d->errorBarStyle; 0317 } 0318 0319 // margin plots 0320 BASIC_SHARED_D_READER_IMPL(XYCurve, bool, rugEnabled, rugEnabled) 0321 BASIC_SHARED_D_READER_IMPL(XYCurve, WorksheetElement::Orientation, rugOrientation, rugOrientation) 0322 BASIC_SHARED_D_READER_IMPL(XYCurve, double, rugLength, rugLength) 0323 BASIC_SHARED_D_READER_IMPL(XYCurve, double, rugWidth, rugWidth) 0324 BASIC_SHARED_D_READER_IMPL(XYCurve, double, rugOffset, rugOffset) 0325 0326 /*! 0327 * return \c true if the data in the source columns (x, y) used in the analysis curves, \c false otherwise 0328 */ 0329 bool XYCurve::isSourceDataChangedSinceLastRecalc() const { 0330 Q_D(const XYCurve); 0331 return d->sourceDataChangedSinceLastRecalc; 0332 } 0333 0334 double XYCurve::minimum(const Dimension) const { 0335 // TODO 0336 return NAN; 0337 } 0338 0339 double XYCurve::maximum(const Dimension) const { 0340 // TODO 0341 return NAN; 0342 } 0343 0344 bool XYCurve::hasData() const { 0345 Q_D(const XYCurve); 0346 return (d->xColumn != nullptr || d->yColumn != nullptr); 0347 } 0348 0349 bool XYCurve::usingColumn(const Column* column) const { 0350 Q_D(const XYCurve); 0351 return (d->xColumn == column || d->yColumn == column || (d->xErrorBar->type() == ErrorBar::Type::Symmetric && d->xErrorBar->plusColumn() == column) 0352 || (d->xErrorBar->type() == ErrorBar::Type::Asymmetric && (d->xErrorBar->plusColumn() == column || d->xErrorBar->minusColumn() == column)) 0353 || (d->yErrorBar->type() == ErrorBar::Type::Symmetric && d->yErrorBar->plusColumn() == column) 0354 || (d->yErrorBar->type() == ErrorBar::Type::Asymmetric && (d->yErrorBar->plusColumn() == column || d->yErrorBar->minusColumn() == column)) 0355 || (d->valuesType == ValuesType::CustomColumn && d->valuesColumn == column)); 0356 } 0357 0358 void XYCurve::updateColumnDependencies(const AbstractColumn* column) { 0359 Q_D(XYCurve); 0360 const QString& columnPath = column->path(); 0361 setUndoAware(false); 0362 0363 if (d->xColumn == column) // the column is the same and was just renamed -> update the column path 0364 d->xColumnPath = columnPath; 0365 else if (d->xColumnPath == columnPath) // another column was renamed to the current path -> set and connect to the new column 0366 setXColumn(column); 0367 0368 if (d->yColumn == column) 0369 d->yColumnPath = columnPath; 0370 else if (d->yColumnPath == columnPath) 0371 setYColumn(column); 0372 0373 if (d->valuesColumn == column) 0374 d->valuesColumnPath = columnPath; 0375 else if (d->valuesColumnPath == columnPath) 0376 setValuesColumn(column); 0377 0378 if (d->valuesColumnPath == columnPath) 0379 setValuesColumn(column); 0380 0381 if (d->xErrorBar->plusColumn() == column) 0382 d->xErrorBar->plusColumnPath() = columnPath; 0383 else if (d->xErrorBar->plusColumnPath() == columnPath) 0384 d->yErrorBar->setPlusColumn(column); 0385 0386 if (d->xErrorBar->minusColumn() == column) 0387 d->xErrorBar->minusColumnPath() = columnPath; 0388 else if (d->xErrorBar->minusColumnPath() == columnPath) 0389 d->xErrorBar->setMinusColumn(column); 0390 0391 if (d->yErrorBar->plusColumn() == column) 0392 d->yErrorBar->plusColumnPath() = columnPath; 0393 else if (d->yErrorBar->plusColumnPath() == columnPath) 0394 d->yErrorBar->setPlusColumn(column); 0395 0396 if (d->yErrorBar->minusColumn() == column) 0397 d->yErrorBar->minusColumnPath() = columnPath; 0398 else if (d->yErrorBar->minusColumnPath() == columnPath) 0399 d->yErrorBar->setMinusColumn(column); 0400 0401 setUndoAware(true); 0402 } 0403 0404 QColor XYCurve::color() const { 0405 Q_D(const XYCurve); 0406 if (d->lineType != XYCurve::LineType::NoLine) 0407 return d->line->pen().color(); 0408 else if (d->symbol->style() != Symbol::Style::NoSymbols) 0409 return d->symbol->pen().color(); 0410 return QColor(); 0411 } 0412 0413 // ############################################################################## 0414 // ################# setter methods and undo commands ########################## 0415 // ############################################################################## 0416 0417 // 1) add XYCurveSetXColumnCmd as friend class to XYCurve 0418 // 2) add XYCURVE_COLUMN_CONNECT(x) as private method to XYCurve 0419 // 3) define all missing slots 0420 CURVE_COLUMN_SETTER_CMD_IMPL_F_S(XYCurve, X, x, recalcLogicalPoints) 0421 void XYCurve::setXColumn(const AbstractColumn* column) { 0422 Q_D(XYCurve); 0423 if (column != d->xColumn) 0424 exec(new XYCurveSetXColumnCmd(d, column, ki18n("%1: x-data source changed"))); 0425 } 0426 0427 CURVE_COLUMN_SETTER_CMD_IMPL_F_S(XYCurve, Y, y, recalcLogicalPoints) 0428 void XYCurve::setYColumn(const AbstractColumn* column) { 0429 Q_D(XYCurve); 0430 if (column != d->yColumn) 0431 exec(new XYCurveSetYColumnCmd(d, column, ki18n("%1: y-data source changed"))); 0432 } 0433 0434 void XYCurve::setXColumnPath(const QString& path) { 0435 Q_D(XYCurve); 0436 d->xColumnPath = path; 0437 } 0438 0439 void XYCurve::setYColumnPath(const QString& path) { 0440 Q_D(XYCurve); 0441 d->yColumnPath = path; 0442 } 0443 0444 // Line 0445 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetLineType, XYCurve::LineType, lineType, updateLines) 0446 void XYCurve::setLineType(LineType type) { 0447 Q_D(XYCurve); 0448 if (type != d->lineType) 0449 exec(new XYCurveSetLineTypeCmd(d, type, ki18n("%1: line type changed"))); 0450 } 0451 0452 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetLineSkipGaps, bool, lineSkipGaps, updateLines) 0453 void XYCurve::setLineSkipGaps(bool skip) { 0454 Q_D(XYCurve); 0455 if (skip != d->lineSkipGaps) 0456 exec(new XYCurveSetLineSkipGapsCmd(d, skip, ki18n("%1: set skip line gaps"))); 0457 } 0458 0459 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetLineIncreasingXOnly, bool, lineIncreasingXOnly, updateLines) 0460 void XYCurve::setLineIncreasingXOnly(bool incr) { 0461 Q_D(XYCurve); 0462 if (incr != d->lineIncreasingXOnly) 0463 exec(new XYCurveSetLineIncreasingXOnlyCmd(d, incr, ki18n("%1: set increasing X"))); 0464 } 0465 0466 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetLineInterpolationPointsCount, int, lineInterpolationPointsCount, updateLines) 0467 void XYCurve::setLineInterpolationPointsCount(int count) { 0468 Q_D(XYCurve); 0469 if (count != d->lineInterpolationPointsCount) 0470 exec(new XYCurveSetLineInterpolationPointsCountCmd(d, count, ki18n("%1: set the number of interpolation points"))); 0471 } 0472 0473 // Values-Tab 0474 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesType, XYCurve::ValuesType, valuesType, updateValues) 0475 void XYCurve::setValuesType(XYCurve::ValuesType type) { 0476 Q_D(XYCurve); 0477 if (type != d->valuesType) 0478 exec(new XYCurveSetValuesTypeCmd(d, type, ki18n("%1: set values type"))); 0479 } 0480 0481 CURVE_COLUMN_SETTER_CMD_IMPL_F_S(XYCurve, Values, values, updateValues) 0482 void XYCurve::setValuesColumn(const AbstractColumn* column) { 0483 Q_D(XYCurve); 0484 if (column != d->valuesColumn) { 0485 exec(new XYCurveSetValuesColumnCmd(d, column, ki18n("%1: set values column"))); 0486 0487 // no need to recalculate the points on value labels changes 0488 disconnect(column, &AbstractColumn::dataChanged, this, &XYCurve::recalcLogicalPoints); 0489 0490 if (column) 0491 connect(column, &AbstractColumn::dataChanged, this, &XYCurve::updateValues); 0492 } 0493 } 0494 0495 void XYCurve::setValuesColumnPath(const QString& path) { 0496 Q_D(XYCurve); 0497 d->valuesColumnPath = path; 0498 } 0499 0500 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesPosition, XYCurve::ValuesPosition, valuesPosition, updateValues) 0501 void XYCurve::setValuesPosition(ValuesPosition position) { 0502 Q_D(XYCurve); 0503 if (position != d->valuesPosition) 0504 exec(new XYCurveSetValuesPositionCmd(d, position, ki18n("%1: set values position"))); 0505 } 0506 0507 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesDistance, qreal, valuesDistance, updateValues) 0508 void XYCurve::setValuesDistance(qreal distance) { 0509 Q_D(XYCurve); 0510 if (distance != d->valuesDistance) 0511 exec(new XYCurveSetValuesDistanceCmd(d, distance, ki18n("%1: set values distance"))); 0512 } 0513 0514 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesRotationAngle, qreal, valuesRotationAngle, updateValues) 0515 void XYCurve::setValuesRotationAngle(qreal angle) { 0516 Q_D(XYCurve); 0517 if (!qFuzzyCompare(1 + angle, 1 + d->valuesRotationAngle)) 0518 exec(new XYCurveSetValuesRotationAngleCmd(d, angle, ki18n("%1: rotate values"))); 0519 } 0520 0521 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesOpacity, qreal, valuesOpacity, updatePixmap) 0522 void XYCurve::setValuesOpacity(qreal opacity) { 0523 Q_D(XYCurve); 0524 if (opacity != d->valuesOpacity) 0525 exec(new XYCurveSetValuesOpacityCmd(d, opacity, ki18n("%1: set values opacity"))); 0526 } 0527 0528 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesNumericFormat, char, valuesNumericFormat, updateValues) 0529 void XYCurve::setValuesNumericFormat(char format) { 0530 Q_D(XYCurve); 0531 if (format != d->valuesNumericFormat) 0532 exec(new XYCurveSetValuesNumericFormatCmd(d, format, ki18n("%1: set values numeric format"))); 0533 } 0534 0535 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesPrecision, int, valuesPrecision, updateValues) 0536 void XYCurve::setValuesPrecision(int precision) { 0537 Q_D(XYCurve); 0538 if (precision != d->valuesPrecision) 0539 exec(new XYCurveSetValuesPrecisionCmd(d, precision, ki18n("%1: set values precision"))); 0540 } 0541 0542 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesDateTimeFormat, QString, valuesDateTimeFormat, updateValues) 0543 void XYCurve::setValuesDateTimeFormat(const QString& format) { 0544 Q_D(XYCurve); 0545 if (format != d->valuesDateTimeFormat) 0546 exec(new XYCurveSetValuesDateTimeFormatCmd(d, format, ki18n("%1: set values datetime format"))); 0547 } 0548 0549 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesPrefix, QString, valuesPrefix, updateValues) 0550 void XYCurve::setValuesPrefix(const QString& prefix) { 0551 Q_D(XYCurve); 0552 if (prefix != d->valuesPrefix) 0553 exec(new XYCurveSetValuesPrefixCmd(d, prefix, ki18n("%1: set values prefix"))); 0554 } 0555 0556 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesSuffix, QString, valuesSuffix, updateValues) 0557 void XYCurve::setValuesSuffix(const QString& suffix) { 0558 Q_D(XYCurve); 0559 if (suffix != d->valuesSuffix) 0560 exec(new XYCurveSetValuesSuffixCmd(d, suffix, ki18n("%1: set values suffix"))); 0561 } 0562 0563 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesFont, QFont, valuesFont, updateValues) 0564 void XYCurve::setValuesFont(const QFont& font) { 0565 Q_D(XYCurve); 0566 if (font != d->valuesFont) 0567 exec(new XYCurveSetValuesFontCmd(d, font, ki18n("%1: set values font"))); 0568 } 0569 0570 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetValuesColor, QColor, valuesColor, updatePixmap) 0571 void XYCurve::setValuesColor(const QColor& color) { 0572 Q_D(XYCurve); 0573 if (color != d->valuesColor) 0574 exec(new XYCurveSetValuesColorCmd(d, color, ki18n("%1: set values color"))); 0575 } 0576 0577 // margin plots 0578 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetRugEnabled, bool, rugEnabled, updateRug) 0579 void XYCurve::setRugEnabled(bool enabled) { 0580 Q_D(XYCurve); 0581 if (enabled != d->rugEnabled) 0582 exec(new XYCurveSetRugEnabledCmd(d, enabled, ki18n("%1: change rug enabled"))); 0583 } 0584 0585 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetRugOrientation, WorksheetElement::Orientation, rugOrientation, updateRug) 0586 void XYCurve::setRugOrientation(WorksheetElement::Orientation orientation) { 0587 Q_D(XYCurve); 0588 if (orientation != d->rugOrientation) 0589 exec(new XYCurveSetRugOrientationCmd(d, orientation, ki18n("%1: set rug orientation"))); 0590 } 0591 0592 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetRugWidth, double, rugWidth, updatePixmap) 0593 void XYCurve::setRugWidth(double width) { 0594 Q_D(XYCurve); 0595 if (width != d->rugWidth) 0596 exec(new XYCurveSetRugWidthCmd(d, width, ki18n("%1: change rug width"))); 0597 } 0598 0599 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetRugLength, double, rugLength, updateRug) 0600 void XYCurve::setRugLength(double length) { 0601 Q_D(XYCurve); 0602 if (length != d->rugLength) 0603 exec(new XYCurveSetRugLengthCmd(d, length, ki18n("%1: change rug length"))); 0604 } 0605 0606 STD_SETTER_CMD_IMPL_F_S(XYCurve, SetRugOffset, double, rugOffset, updateRug) 0607 void XYCurve::setRugOffset(double offset) { 0608 Q_D(XYCurve); 0609 if (offset != d->rugOffset) 0610 exec(new XYCurveSetRugOffsetCmd(d, offset, ki18n("%1: change rug offset"))); 0611 } 0612 0613 // ############################################################################## 0614 // ################################# SLOTS #################################### 0615 // ############################################################################## 0616 void XYCurve::retransform() { 0617 Q_D(XYCurve); 0618 d->retransform(); 0619 } 0620 0621 void XYCurve::recalcLogicalPoints() { 0622 Q_D(XYCurve); 0623 d->recalcLogicalPoints(); 0624 } 0625 0626 void XYCurve::updateValues() { 0627 Q_D(XYCurve); 0628 d->updateValues(); 0629 } 0630 0631 void XYCurve::updateErrorBars() { 0632 Q_D(XYCurve); 0633 d->updateErrorBars(); 0634 } 0635 0636 // TODO 0637 void XYCurve::handleResize(double horizontalRatio, double verticalRatio, bool /*pageResize*/) { 0638 Q_D(const XYCurve); 0639 0640 d->symbol->setSize(d->symbol->size() * horizontalRatio); 0641 0642 QPen pen = d->symbol->pen(); 0643 pen.setWidthF(pen.widthF() * (horizontalRatio + verticalRatio) / 2.); 0644 d->symbol->setPen(pen); 0645 0646 double width = d->line->width() * (horizontalRatio + verticalRatio) / 2.; 0647 d->line->setWidth(width); 0648 0649 // setValuesDistance(d->distance*); 0650 QFont font = d->valuesFont; 0651 font.setPointSizeF(font.pointSizeF() * horizontalRatio); 0652 setValuesFont(font); 0653 } 0654 0655 // Finds index where x is located and returns the value "index" after the found value 0656 /*! 0657 * Find nearest x value from a value xpos and his y value 0658 * @param xpos position for which the next index xpos should be found 0659 * @param offset Offset from the index where xpos is. Positive is after the found index, negative is before the found index 0660 * @param x x value at the found index 0661 * @param y y value at the found index 0662 * @param valueFound True when value found, otherwise false 0663 */ 0664 int XYCurve::getNextValue(double xpos, int offset, double& x, double& y, bool& valueFound) const { 0665 valueFound = false; 0666 auto properties = xColumn()->properties(); 0667 if (properties == AbstractColumn::Properties::MonotonicDecreasing) 0668 offset *= -1; 0669 0670 int index = xColumn()->indexForValue(xpos); 0671 if (index < 0) 0672 return -1; 0673 if (offset > 0 && index + offset < xColumn()->rowCount()) 0674 index += offset; 0675 else if (offset > 0) 0676 index = xColumn()->rowCount() - 1; 0677 else if ((offset < 0 && index + offset > 0)) 0678 index += offset; 0679 else 0680 index = 0; 0681 0682 auto xMode = xColumn()->columnMode(); 0683 0684 if (xMode == AbstractColumn::ColumnMode::Double || xMode == AbstractColumn::ColumnMode::Integer) 0685 x = xColumn()->valueAt(index); 0686 else if (xMode == AbstractColumn::ColumnMode::DateTime || xMode == AbstractColumn::ColumnMode::Day || xMode == AbstractColumn::ColumnMode::Month) 0687 x = xColumn()->dateTimeAt(index).toMSecsSinceEpoch(); 0688 else 0689 return index; 0690 0691 auto yMode = yColumn()->columnMode(); 0692 0693 if (yMode == AbstractColumn::ColumnMode::Double || yMode == AbstractColumn::ColumnMode::Integer) 0694 y = yColumn()->valueAt(index); 0695 else if (yMode == AbstractColumn::ColumnMode::DateTime || yMode == AbstractColumn::ColumnMode::Day || yMode == AbstractColumn::ColumnMode::Month) 0696 y = yColumn()->dateTimeAt(index).toMSecsSinceEpoch(); 0697 else 0698 return index; 0699 0700 valueFound = true; 0701 return index; 0702 } 0703 0704 void XYCurve::xColumnAboutToBeRemoved(const AbstractAspect* aspect) { 0705 Q_D(XYCurve); 0706 if (aspect == d->xColumn) { 0707 d->xColumn = nullptr; 0708 d->m_logicalPoints.clear(); 0709 d->retransform(); 0710 } 0711 } 0712 0713 void XYCurve::yColumnAboutToBeRemoved(const AbstractAspect* aspect) { 0714 Q_D(XYCurve); 0715 if (aspect == d->yColumn) { 0716 d->yColumn = nullptr; 0717 d->m_logicalPoints.clear(); 0718 d->retransform(); 0719 } 0720 } 0721 0722 void XYCurve::valuesColumnAboutToBeRemoved(const AbstractAspect* aspect) { 0723 Q_D(XYCurve); 0724 if (aspect == d->valuesColumn) { 0725 d->valuesColumn = nullptr; 0726 d->updateValues(); 0727 } 0728 } 0729 0730 // ############################################################################## 0731 // ###### SLOTs for changes triggered via QActions in the context menu ######## 0732 // ############################################################################## 0733 0734 void XYCurve::navigateTo() { 0735 project()->navigateTo(navigateToAction->data().toString()); 0736 } 0737 0738 // ############################################################################## 0739 // ######################### Private implementation ############################# 0740 // ############################################################################## 0741 XYCurvePrivate::XYCurvePrivate(XYCurve* owner) 0742 : PlotPrivate(owner) 0743 , q(owner) { 0744 setFlag(QGraphicsItem::ItemIsSelectable, true); 0745 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); 0746 setAcceptHoverEvents(false); 0747 } 0748 0749 void XYCurvePrivate::calculateScenePoints() { 0750 if (!q->plot() || !m_scenePointsDirty || !xColumn) 0751 return; 0752 #if PERFTRACE_CURVES 0753 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name()); 0754 #endif 0755 0756 m_scenePoints.clear(); 0757 0758 // calculate the scene coordinates 0759 // This condition cannot be used, because m_logicalPoints is also used in updateErrorBars(), updateDropLines() and in updateFilling() 0760 // TODO: check updateErrorBars() and updateDropLines() and if they aren't available don't calculate this part 0761 // if (symbolsStyle != Symbol::Style::NoSymbols || valuesType != XYCurve::NoValues ) { 0762 { 0763 #if PERFTRACE_CURVES 0764 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name() + QStringLiteral(", map logical points to scene coordinates")); 0765 #endif 0766 0767 const int numberOfPoints = m_logicalPoints.size(); 0768 DEBUG(Q_FUNC_INFO << ", number of logical points = " << numberOfPoints) 0769 // for (auto p : m_logicalPoints) 0770 // QDEBUG(Q_FUNC_INFO << ", logical points: " << QString::number(p.x(), 'g', 12) << " = " << QDateTime::fromMSecsSinceEpoch(p.x(), Qt::UTC)) 0771 0772 if (numberOfPoints > 0) { 0773 const auto dataRect{plot()->dataRect()}; 0774 // this is the old method considering DPI 0775 DEBUG(Q_FUNC_INFO << ", plot->dataRect() width/height = " << dataRect.width() << '/' << dataRect.height()); 0776 DEBUG(Q_FUNC_INFO << ", logical DPI X/Y = " << QApplication::primaryScreen()->logicalDotsPerInchX() << '/' 0777 << QApplication::primaryScreen()->logicalDotsPerInchY()) 0778 DEBUG(Q_FUNC_INFO << ", physical DPI X/Y = " << QApplication::primaryScreen()->physicalDotsPerInchX() << '/' 0779 << QApplication::primaryScreen()->physicalDotsPerInchY()) 0780 0781 // new method 0782 const int numberOfPixelX = dataRect.width(); 0783 const int numberOfPixelY = dataRect.height(); 0784 0785 if (numberOfPixelX <= 0 || numberOfPixelY <= 0) { 0786 DEBUG(Q_FUNC_INFO << ", number of pixel X <= 0 or number of pixel Y <= 0!") 0787 return; 0788 } 0789 DEBUG(" numberOfPixelX/numberOfPixelY = " << numberOfPixelX << '/' << numberOfPixelY) 0790 0791 // eliminate multiple scene points (size (numberOfPixelX + 1) * (numberOfPixelY + 1)) TODO: why "+1" 0792 QVector<QVector<bool>> scenePointsUsed(numberOfPixelX + 1); 0793 for (auto& col : scenePointsUsed) 0794 col.resize(numberOfPixelY + 1); 0795 0796 const auto columnProperties = xColumn->properties(); 0797 int startIndex, endIndex; 0798 if (columnProperties == AbstractColumn::Properties::MonotonicDecreasing || columnProperties == AbstractColumn::Properties::MonotonicIncreasing) { 0799 DEBUG(Q_FUNC_INFO << ", column monotonic") 0800 if (!q->cSystem->isValid()) 0801 return; 0802 double xMin = q->cSystem->mapSceneToLogical(dataRect.topLeft()).x(); 0803 double xMax = q->cSystem->mapSceneToLogical(dataRect.bottomRight()).x(); 0804 DEBUG(Q_FUNC_INFO << ", xMin/xMax = " << xMin << '/' << xMax) 0805 0806 startIndex = Column::indexForValue(xMin, m_logicalPoints, columnProperties); 0807 endIndex = Column::indexForValue(xMax, m_logicalPoints, columnProperties); 0808 0809 if (startIndex > endIndex && endIndex >= 0) 0810 std::swap(startIndex, endIndex); 0811 0812 if (startIndex < 0) 0813 startIndex = 0; 0814 if (endIndex < 0) 0815 endIndex = numberOfPoints - 1; 0816 0817 } else { 0818 DEBUG(Q_FUNC_INFO << ", column not monotonic") 0819 startIndex = 0; 0820 endIndex = numberOfPoints - 1; 0821 } 0822 //} // (symbolsStyle != Symbol::NoSymbols || valuesType != XYCurve::NoValues ) 0823 0824 m_pointVisible.resize(numberOfPoints); 0825 q->cSystem->mapLogicalToScene(startIndex, endIndex, m_logicalPoints, m_scenePoints, m_pointVisible); 0826 // for (auto p : m_logicalPoints) 0827 // QDEBUG(Q_FUNC_INFO << ", logical points: " << QString::number(p.x(), 'g', 12) << " = " << QDateTime::fromMSecsSinceEpoch(p.x(), Qt::UTC)) 0828 } 0829 } 0830 //} // (symbolsStyle != Symbol::Style::NoSymbols || valuesType != XYCurve::NoValues ) 0831 m_scenePointsDirty = false; 0832 } 0833 0834 /*! 0835 called when the size of the plot or its data ranges (manual changes, zooming, etc.) were changed. 0836 recalculates the position of the scene points to be drawn. 0837 triggers the update of lines, drop lines, symbols etc. 0838 */ 0839 void XYCurvePrivate::retransform() { 0840 const bool suppressed = !isVisible() || q->isLoading() || suppressRetransform || !plot(); 0841 DEBUG("\n" << Q_FUNC_INFO << ", name = " << STDSTRING(name()) << ", suppressRetransform = " << suppressRetransform); 0842 trackRetransformCalled(suppressed); 0843 if (suppressed) 0844 return; 0845 0846 m_scenePointsDirty = true; 0847 m_scenePoints.clear(); // free memory 0848 0849 DEBUG(Q_FUNC_INFO << ", x/y column = " << xColumn << "/" << yColumn); 0850 // Q_ASSERT(xColumn != nullptr); 0851 if (!xColumn || !yColumn) { 0852 DEBUG(Q_FUNC_INFO << ", WARNING: xColumn or yColumn not available"); 0853 linePath = QPainterPath(); 0854 dropLinePath = QPainterPath(); 0855 symbolsPath = QPainterPath(); 0856 valuesPath = QPainterPath(); 0857 errorBarsPath = QPainterPath(); 0858 rugPath = QPainterPath(); 0859 m_shape = QPainterPath(); 0860 m_lines.clear(); 0861 m_valuePoints.clear(); 0862 m_valueStrings.clear(); 0863 m_fillPolygons.clear(); 0864 recalcShapeAndBoundingRect(); 0865 return; 0866 } 0867 0868 suppressRecalc = true; 0869 updateLines(); 0870 updateDropLines(); 0871 updateSymbols(); 0872 updateRug(); 0873 updateValues(); 0874 suppressRecalc = false; 0875 updateErrorBars(); 0876 } 0877 0878 /*! 0879 * called if the x- or y-data was changed. 0880 * copies the valid data points from the x- and y-columns into the internal container 0881 */ 0882 void XYCurvePrivate::recalcLogicalPoints() { 0883 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name()); 0884 0885 m_pointVisible.clear(); 0886 m_logicalPoints.clear(); 0887 connectedPointsLogical.clear(); 0888 validPointsIndicesLogical.clear(); 0889 0890 if (!xColumn || !yColumn) 0891 return; 0892 0893 auto xColMode = xColumn->columnMode(); 0894 auto yColMode = yColumn->columnMode(); 0895 const int rows = xColumn->rowCount(); 0896 m_logicalPoints.reserve(rows); 0897 0898 // take only valid and non masked points 0899 for (int row = 0; row < rows; row++) { 0900 if (xColumn->isValid(row) && yColumn->isValid(row) && (!xColumn->isMasked(row)) && (!yColumn->isMasked(row))) { 0901 QPointF tempPoint; 0902 0903 switch (xColMode) { 0904 case AbstractColumn::ColumnMode::Double: 0905 tempPoint.setX(xColumn->doubleAt(row)); 0906 break; 0907 case AbstractColumn::ColumnMode::Integer: 0908 tempPoint.setX(xColumn->integerAt(row)); 0909 break; 0910 case AbstractColumn::ColumnMode::BigInt: 0911 tempPoint.setX(xColumn->bigIntAt(row)); 0912 break; 0913 case AbstractColumn::ColumnMode::DateTime: 0914 tempPoint.setX(xColumn->dateTimeAt(row).toMSecsSinceEpoch()); 0915 break; 0916 case AbstractColumn::ColumnMode::Text: 0917 case AbstractColumn::ColumnMode::Month: 0918 case AbstractColumn::ColumnMode::Day: 0919 break; 0920 } 0921 0922 switch (yColMode) { 0923 case AbstractColumn::ColumnMode::Double: 0924 tempPoint.setY(yColumn->doubleAt(row)); 0925 break; 0926 case AbstractColumn::ColumnMode::Integer: 0927 tempPoint.setY(yColumn->integerAt(row)); 0928 break; 0929 case AbstractColumn::ColumnMode::BigInt: 0930 tempPoint.setY(yColumn->bigIntAt(row)); 0931 break; 0932 case AbstractColumn::ColumnMode::DateTime: 0933 tempPoint.setY(yColumn->dateTimeAt(row).toMSecsSinceEpoch()); 0934 break; 0935 case AbstractColumn::ColumnMode::Text: 0936 case AbstractColumn::ColumnMode::Month: 0937 case AbstractColumn::ColumnMode::Day: 0938 break; 0939 } 0940 0941 m_logicalPoints.append(tempPoint); 0942 // TODO: append, resize-reserve 0943 connectedPointsLogical.push_back(true); 0944 validPointsIndicesLogical.push_back(row); 0945 } else { 0946 if (!connectedPointsLogical.empty()) 0947 connectedPointsLogical[connectedPointsLogical.size() - 1] = false; 0948 } 0949 } 0950 0951 m_pointVisible.resize(m_logicalPoints.size()); 0952 } 0953 0954 /*! 0955 * Adds a line, which connects two points, but only if they don't lie on the same xAxis pixel. 0956 * If they lie on the same x pixel, draw a vertical line between the minimum and maximum y value. So all points are included 0957 * This function can be used for all axis scalings (linear, log, sqrt, ...). For the linear case use the function above, because it's optimized for the linear 0958 * case 0959 * @param p next point 0960 * @param x 0961 * @param minY 0962 * @param maxY 0963 * @param lastPoint remember last point in case of overlap 0964 * @param pixelDiff x pixel distance between two points 0965 * @param pixelCount pixel count 0966 */ 0967 void XYCurvePrivate::addLine(QPointF p, 0968 double& x, 0969 double& minY, 0970 double& maxY, 0971 QPointF& lastPoint, 0972 int& pixelDiff, 0973 int numberOfPixelX, 0974 double minDiffX, 0975 RangeT::Scale scale, 0976 bool& prevPixelDiffZero) { 0977 if (scale == RangeT::Scale::Linear) { 0978 pixelDiff = (std::round(p.x() / minDiffX) - x) != 0; // only relevant if greater zero or not 0979 addUniqueLine(p, minY, maxY, lastPoint, pixelDiff, m_lines, prevPixelDiffZero); 0980 if (pixelDiff > 0) // set x to next pixel 0981 x = std::round(p.x() / minDiffX); 0982 } else { 0983 // for nonlinear scaling the pixel distance must be calculated for every point 0984 static const double preCalc = (double)plot()->dataRect().width() / numberOfPixelX; 0985 bool visible; 0986 QPointF pScene = q->cSystem->mapLogicalToScene(p, visible, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping); 0987 0988 // if the point is not valid, don't create a line 0989 if (!visible) 0990 return; 0991 0992 // using only the difference between the points is not sufficient, because 0993 // p0 is updated always independent if new line added or not 0994 const int p1Pixel = std::round((pScene.x() - plot()->dataRect().x()) / preCalc); 0995 pixelDiff = p1Pixel - x; 0996 0997 addUniqueLine(p, minY, maxY, lastPoint, pixelDiff, m_lines, prevPixelDiffZero); 0998 0999 if (pixelDiff > 0) // set x to next pixel 1000 x = std::round((pScene.x() - plot()->dataRect().x()) / preCalc); 1001 } 1002 } 1003 1004 /*! 1005 * \brief XYCurvePrivate::addUniqueLine 1006 * This function is called from the other two addLine() functions to avoid duplication 1007 * @param p next point 1008 * @param minY 1009 * @param maxY 1010 * @param lastPoint remember last point in case of overlap 1011 * @param pixelDiff pixel distance in x between two points 1012 */ 1013 void XYCurvePrivate::addUniqueLine(QPointF p, double& minY, double& maxY, QPointF& lastPoint, int& pixelDiff, QVector<QLineF>& lines, bool& prevPixelDiffZero) { 1014 if (pixelDiff == 0) { 1015 maxY = std::max(p.y(), maxY); 1016 minY = std::min(p.y(), minY); 1017 prevPixelDiffZero = true; 1018 } else { 1019 if (prevPixelDiffZero) { 1020 // If previously more than one point lied on the same pixel 1021 // a vertical line will be drawn which connects all points 1022 // on this x value. So we don't have to draw a lot of lines, 1023 // but only one 1024 1025 // Not needed, because the line would be that small that it will never be visible 1026 // if (m_lines.count() > 0) { 1027 // auto p_temp = m_lines.last().p2(); 1028 // m_lines.append(QLineF(p_temp.x(), p_temp.y(), x, p_temp.y())); 1029 // } 1030 if (maxY != minY) 1031 lines.append(QLineF(lastPoint.x(), minY, lastPoint.x(), maxY)); 1032 lines.append(QLineF(lastPoint, p)); 1033 } else if (!std::isnan(lastPoint.x()) && !std::isnan(lastPoint.y())) 1034 lines.append(QLineF(lastPoint, p)); 1035 prevPixelDiffZero = false; 1036 minY = p.y(); 1037 maxY = p.y(); 1038 // TODO: needed? 1039 // if (p1.y() >= minY && p1.y() <= maxY && pixelDiff == 1) 1040 // return; 1041 } 1042 lastPoint = p; 1043 } 1044 1045 /*! 1046 recalculates the painter path for the lines connecting the data points. 1047 Called each time when the type of this connection is changed. 1048 TODO: At the moment also the points which are outside of the scene are added. This algorithm can be improved by omitting all 1049 lines where both points are outside of the scene 1050 */ 1051 void XYCurvePrivate::updateLines() { 1052 #if PERFTRACE_CURVES 1053 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name()); 1054 #endif 1055 linePath = QPainterPath(); 1056 m_lines.clear(); 1057 if (lineType == XYCurve::LineType::NoLine) { 1058 DEBUG(Q_FUNC_INFO << ", nothing to do, since line type is XYCurve::LineType::NoLine"); 1059 updateFilling(); 1060 recalcShapeAndBoundingRect(); 1061 return; 1062 } 1063 1064 int numberOfPoints{static_cast<int>(m_logicalPoints.size())}; 1065 if (numberOfPoints <= 1) { 1066 DEBUG(Q_FUNC_INFO << ", nothing to do, since not enough data points available"); 1067 recalcShapeAndBoundingRect(); 1068 return; 1069 } 1070 1071 const QRectF pageRect = plot()->dataRect(); 1072 // old method using DPI 1073 // const double widthDatarectInch = Worksheet::convertFromSceneUnits(plot()->dataRect().width(), Worksheet::Unit::Inch); 1074 // float heightDatarectInch = Worksheet::convertFromSceneUnits(plot()->dataRect().height(), Worksheet::Unit::Inch); 1075 // const int numberOfPixelX = ceil(widthDatarectInch * QApplication::desktop()->physicalDpiX()); 1076 const int numberOfPixelX = pageRect.width(); 1077 1078 // calculate the lines connecting the data points 1079 { 1080 #if PERFTRACE_CURVES 1081 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name() + QStringLiteral(", calculate the lines connecting the data points")); 1082 #endif 1083 1084 // find index for xMin and xMax to not loop through all values 1085 int startIndex, endIndex; 1086 auto columnProperties = q->xColumn()->properties(); 1087 if (columnProperties == AbstractColumn::Properties::MonotonicDecreasing || columnProperties == AbstractColumn::Properties::MonotonicIncreasing) { 1088 DEBUG(Q_FUNC_INFO << ", monotonic") 1089 if (!q->cSystem->isValid()) 1090 return; 1091 const double xMin = q->cSystem->mapSceneToLogical(pageRect.topLeft()).x(); 1092 const double xMax = q->cSystem->mapSceneToLogical(pageRect.bottomRight()).x(); 1093 1094 startIndex = Column::indexForValue(xMin, m_logicalPoints, columnProperties); 1095 endIndex = Column::indexForValue(xMax, m_logicalPoints, columnProperties); 1096 1097 if (startIndex > endIndex) 1098 std::swap(startIndex, endIndex); 1099 1100 startIndex--; // use one value before 1101 endIndex++; 1102 if (startIndex < 0) 1103 startIndex = 0; 1104 if (endIndex < 0 || endIndex >= numberOfPoints) 1105 endIndex = numberOfPoints - 1; 1106 1107 numberOfPoints = endIndex - startIndex + 1; 1108 } else { 1109 DEBUG(Q_FUNC_INFO << ", non monotonic") 1110 startIndex = 0; 1111 endIndex = numberOfPoints - 1; 1112 } 1113 DEBUG(Q_FUNC_INFO << ", start/endIndex = " << startIndex << '/' << endIndex) 1114 1115 QPointF tempPoint1, tempPoint2; // used as temporaryPoints to interpolate datapoints if set 1116 if (columnProperties == AbstractColumn::Properties::Constant) { 1117 DEBUG(Q_FUNC_INFO << ", CONSTANT column") 1118 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1119 const auto xRange{plot()->range(Dimension::X, cs->index(Dimension::X))}; 1120 const auto yRange{plot()->range(Dimension::Y, cs->index(Dimension::Y))}; 1121 tempPoint1 = QPointF(xRange.start(), yRange.start()); 1122 tempPoint2 = QPointF(xRange.start(), yRange.end()); 1123 m_lines.append(QLineF(tempPoint1, tempPoint2)); 1124 } else { 1125 QPointF lastPoint{NAN, NAN}; // last x value 1126 int pixelDiff = 0; 1127 bool prevPixelDiffZero = false; 1128 double minY{INFINITY}, maxY{-INFINITY}; 1129 QPointF p0, p1; 1130 const auto xIndex{q->cSystem->index(Dimension::X)}; 1131 const auto xRange{plot()->range(Dimension::X, xIndex)}; 1132 double minDiffX; 1133 const RangeT::Scale scale = plot()->xRangeScale(xIndex); 1134 1135 // setting initial point 1136 double xPos = NAN; 1137 if (scale == RangeT::Scale::Linear) 1138 minDiffX = (xRange.end() - xRange.start()) / numberOfPixelX; 1139 else { 1140 // For nonlinear x achses, the linear scene coordinates are used 1141 minDiffX = plot()->dataRect().width() / numberOfPixelX; 1142 } 1143 1144 // determine first point 1145 for (int i{startIndex}; i < endIndex; i++) { 1146 if (!lineSkipGaps && !connectedPointsLogical.at(i)) 1147 continue; 1148 1149 p0 = m_logicalPoints.at(i); 1150 startIndex = i + 1; 1151 1152 if (lineType != XYCurve::LineType::SplineCubicNatural && lineType != XYCurve::LineType::SplineCubicPeriodic 1153 && lineType != XYCurve::LineType::SplineAkimaNatural && lineType != XYCurve::LineType::SplineAkimaPeriodic) { 1154 if (scale == RangeT::Scale::Linear) { 1155 xPos = std::round(p0.x() / minDiffX); 1156 lastPoint = p0; 1157 minY = p0.y(); 1158 maxY = p0.y(); 1159 } else { 1160 bool visible; 1161 QPointF pScene = q->cSystem->mapLogicalToScene(p0, visible, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping); 1162 if (!visible) 1163 continue; 1164 xPos = std::round((pScene.x() - plot()->dataRect().x()) / ((double)plot()->dataRect().width() * numberOfPixelX)); 1165 lastPoint = p0; 1166 } 1167 } 1168 break; 1169 } 1170 1171 switch (lineType) { 1172 case XYCurve::LineType::NoLine: 1173 break; 1174 case XYCurve::LineType::Line: { 1175 #if PERFTRACE_CURVES 1176 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO) + QStringLiteral(", find relevant lines")); 1177 #endif 1178 for (int i{startIndex}; i <= endIndex; i++) { 1179 p1 = m_logicalPoints.at(i); 1180 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1181 if (pixelDiff == 0) 1182 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1183 prevPixelDiffZero = false; 1184 p0 = p1; 1185 lastPoint = p1; 1186 continue; 1187 } 1188 1189 if (lineIncreasingXOnly && (p1.x() < p0.x())) // skip points 1190 continue; 1191 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1192 p0 = p1; 1193 } 1194 1195 if (pixelDiff == 0) 1196 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1197 1198 break; 1199 } 1200 case XYCurve::LineType::StartHorizontal: { 1201 for (int i{startIndex}; i <= endIndex; i++) { 1202 p1 = m_logicalPoints.at(i); 1203 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1204 // if (pixelDiff == 0) // not needed, because last line will be always a vertical 1205 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1206 prevPixelDiffZero = false; 1207 p0 = p1; 1208 lastPoint = p1; 1209 continue; 1210 } 1211 if (lineIncreasingXOnly && (p1.x() < p0.x())) 1212 continue; 1213 1214 tempPoint1 = QPointF(p1.x(), p0.y()); // horizontal line 1215 addLine(tempPoint1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1216 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1217 p0 = p1; 1218 } 1219 // last line is a vertical line so it must be drawn manually (pixelDiff is 0) 1220 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1221 break; 1222 } 1223 case XYCurve::LineType::StartVertical: { 1224 for (int i{startIndex}; i <= endIndex; i++) { 1225 p1 = m_logicalPoints.at(i); 1226 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1227 if (pixelDiff == 0) 1228 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1229 prevPixelDiffZero = false; 1230 p0 = p1; 1231 lastPoint = p1; 1232 continue; 1233 } 1234 if (lineIncreasingXOnly && (p1.x() < p0.x())) 1235 continue; 1236 tempPoint1 = QPointF(p0.x(), p1.y()); 1237 addLine(tempPoint1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1238 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1239 p0 = p1; 1240 } 1241 1242 if (pixelDiff == 0) 1243 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1244 1245 break; 1246 } 1247 case XYCurve::LineType::MidpointHorizontal: { 1248 for (int i{startIndex}; i <= endIndex; i++) { 1249 p1 = m_logicalPoints.at(i); 1250 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1251 if (pixelDiff == 0) 1252 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1253 prevPixelDiffZero = false; 1254 p0 = p1; 1255 lastPoint = p1; 1256 continue; 1257 } 1258 if (lineIncreasingXOnly && (p1.x() < p0.x())) 1259 continue; 1260 tempPoint1 = QPointF(p0.x() + (p1.x() - p0.x()) / 2., p0.y()); // horizontal until mid at p0.y level 1261 tempPoint2 = QPointF(p0.x() + (p1.x() - p0.x()) / 2., p1.y()); // vertical line 1262 addLine(tempPoint1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1263 addLine(tempPoint2, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1264 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1265 p0 = p1; 1266 } 1267 1268 if (pixelDiff == 0) 1269 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1270 1271 break; 1272 } 1273 case XYCurve::LineType::MidpointVertical: { 1274 for (int i{startIndex}; i <= endIndex; i++) { 1275 p1 = m_logicalPoints.at(i); 1276 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1277 // if (pixelDiff == 0) // last line will be always a vertical line 1278 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1279 prevPixelDiffZero = false; 1280 p0 = p1; 1281 lastPoint = p1; 1282 continue; 1283 } 1284 if (lineIncreasingXOnly && (p1.x() < p0.x())) 1285 continue; 1286 tempPoint1 = QPointF(p0.x(), p0.y() + (p1.y() - p0.y()) / 2.); 1287 tempPoint2 = QPointF(p1.x(), p0.y() + (p1.y() - p0.y()) / 2.); 1288 addLine(tempPoint1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1289 addLine(tempPoint2, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1290 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1291 p0 = p1; 1292 } 1293 // last line is a vertical line so it must be drawn manually (pixelDiff is 0) 1294 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1295 break; 1296 } 1297 case XYCurve::LineType::Segments2: 1298 case XYCurve::LineType::Segments3: { 1299 int skip{0}; 1300 int skip_index = 1; 1301 if (lineType == XYCurve::LineType::Segments2) 1302 skip_index = 1; 1303 else if (lineType == XYCurve::LineType::Segments3) 1304 skip_index = 2; 1305 else 1306 DEBUG("WARNING: unhandled case!!!!"); 1307 1308 for (int i{startIndex}; i <= endIndex; i++) { 1309 p1 = m_logicalPoints.at(i); 1310 if (skip < skip_index) { 1311 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1312 if (pixelDiff == 0) 1313 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1314 prevPixelDiffZero = false; 1315 p0 = p1; 1316 lastPoint = p1; 1317 skip = 0; 1318 continue; 1319 } 1320 1321 if (lineIncreasingXOnly && (p1.x() < p0.x())) { 1322 skip = 0; 1323 continue; 1324 } 1325 addLine(p1, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1326 skip++; 1327 } else { 1328 skip = 0; 1329 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1330 if (pixelDiff == 0) 1331 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1332 prevPixelDiffZero = false; 1333 skip += 2; // if one point is missing, 2 lines are skipped 1334 } 1335 lastPoint = p1; 1336 } 1337 p0 = p1; 1338 } 1339 1340 if (pixelDiff == 0 && skip != skip_index) 1341 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1342 1343 break; 1344 } 1345 case XYCurve::LineType::SplineCubicNatural: 1346 case XYCurve::LineType::SplineCubicPeriodic: 1347 case XYCurve::LineType::SplineAkimaNatural: 1348 case XYCurve::LineType::SplineAkimaPeriodic: { 1349 std::unique_ptr<double[]> x(new double[numberOfPoints]()); 1350 std::unique_ptr<double[]> y(new double[numberOfPoints]()); 1351 int validPoints = 1; // p0 is valid 1352 x[0] = p0.x(); 1353 y[0] = p0.y(); 1354 for (int i{startIndex}; i <= endIndex; i++) { 1355 p1 = m_logicalPoints.at(i); 1356 if (!lineSkipGaps && (i > startIndex && !connectedPointsLogical.at(i - 1))) { 1357 if (pixelDiff == 0) 1358 m_lines.append(QLineF(QPointF(p0.x(), minY), QPointF(p0.x(), maxY))); 1359 p0 = p1; 1360 lastPoint = p1; 1361 continue; 1362 } 1363 if (lineIncreasingXOnly && (p1.x() < p0.x())) 1364 continue; 1365 validPoints++; 1366 x[i] = p1.x(); 1367 y[i] = p1.y(); 1368 } 1369 numberOfPoints = validPoints; 1370 1371 gsl_interp_accel* acc = gsl_interp_accel_alloc(); 1372 gsl_spline* spline{nullptr}; 1373 gsl_set_error_handler_off(); 1374 switch (lineType) { 1375 case XYCurve::LineType::SplineCubicNatural: 1376 spline = gsl_spline_alloc(gsl_interp_cspline, numberOfPoints); 1377 break; 1378 case XYCurve::LineType::SplineCubicPeriodic: 1379 spline = gsl_spline_alloc(gsl_interp_cspline_periodic, numberOfPoints); 1380 break; 1381 case XYCurve::LineType::SplineAkimaNatural: 1382 spline = gsl_spline_alloc(gsl_interp_akima, numberOfPoints); 1383 break; 1384 case XYCurve::LineType::SplineAkimaPeriodic: 1385 spline = gsl_spline_alloc(gsl_interp_akima_periodic, numberOfPoints); 1386 break; 1387 case XYCurve::LineType::NoLine: 1388 case XYCurve::LineType::Line: 1389 case XYCurve::LineType::StartHorizontal: 1390 case XYCurve::LineType::StartVertical: 1391 case XYCurve::LineType::MidpointHorizontal: 1392 case XYCurve::LineType::MidpointVertical: 1393 case XYCurve::LineType::Segments2: 1394 case XYCurve::LineType::Segments3: 1395 break; 1396 } 1397 1398 if (!spline) { 1399 QString msg; 1400 if ((lineType == XYCurve::LineType::SplineAkimaNatural || lineType == XYCurve::LineType::SplineAkimaPeriodic) && numberOfPoints < 5) 1401 msg = i18n("Error: Akima spline interpolation requires a minimum of 5 points."); 1402 else 1403 msg = i18n("Error: Could not initialize the spline function."); 1404 Q_EMIT q->info(msg); 1405 1406 recalcShapeAndBoundingRect(); 1407 gsl_interp_accel_free(acc); 1408 return; 1409 } 1410 1411 int status = gsl_spline_init(spline, x.get(), y.get(), numberOfPoints); 1412 if (status != 0) { 1413 // TODO: check in gsl/interp.c when GSL_EINVAL is thrown 1414 QString gslError; 1415 if (status == GSL_EINVAL) 1416 gslError = i18n("x values must be monotonically increasing."); 1417 else 1418 gslError = gslErrorToString(status); 1419 Q_EMIT q->info(i18n("Error: %1", gslError)); 1420 1421 recalcShapeAndBoundingRect(); 1422 gsl_spline_free(spline); 1423 gsl_interp_accel_free(acc); 1424 return; 1425 } 1426 1427 // create interpolating points 1428 // TODO: QVector 1429 std::vector<double> xinterp, yinterp; 1430 for (int i{0}; i < numberOfPoints - 1; i++) { 1431 const double x1 = x[i]; 1432 const double x2 = x[i + 1]; 1433 const double step = std::abs(x2 - x1) / (lineInterpolationPointsCount + 1); 1434 1435 for (int j{0}; j < (lineInterpolationPointsCount + 1); j++) { 1436 const double xi = x1 + j * step; 1437 const double yi = gsl_spline_eval(spline, xi, acc); 1438 xinterp.push_back(xi); 1439 yinterp.push_back(yi); 1440 } 1441 } 1442 1443 if (!xinterp.empty()) { 1444 for (unsigned int i{0}; i < xinterp.size() - 1; i++) { 1445 p0 = QPointF(xinterp[i], yinterp[i]); 1446 addLine(p0, xPos, minY, maxY, lastPoint, pixelDiff, numberOfPixelX, minDiffX, scale, prevPixelDiffZero); 1447 } 1448 1449 addLine(QPointF(x[numberOfPoints - 1], y[numberOfPoints - 1]), 1450 xPos, 1451 minY, 1452 maxY, 1453 lastPoint, 1454 pixelDiff, 1455 numberOfPixelX, 1456 minDiffX, 1457 scale, 1458 prevPixelDiffZero); 1459 } 1460 1461 gsl_spline_free(spline); 1462 gsl_interp_accel_free(acc); 1463 break; 1464 } 1465 } 1466 } 1467 } 1468 1469 // map the lines to scene coordinates 1470 { 1471 #if PERFTRACE_CURVES 1472 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name() + QStringLiteral(", map lines to scene coordinates")); 1473 #endif 1474 Q_EMIT q->linesUpdated(q, m_lines); 1475 m_lines = q->cSystem->mapLogicalToScene(m_lines); 1476 } 1477 1478 { 1479 #if PERFTRACE_CURVES 1480 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name() + QStringLiteral(", calculate new line path")); 1481 #endif 1482 // new line path 1483 if (!m_lines.isEmpty()) { 1484 linePath.moveTo(m_lines.constFirst().p1()); 1485 QPointF prevP2; 1486 for (const auto& line : qAsConst(m_lines)) { 1487 if (prevP2 != line.p1()) 1488 linePath.moveTo(line.p1()); 1489 linePath.lineTo(line.p2()); 1490 prevP2 = line.p2(); 1491 } 1492 } 1493 } 1494 1495 updateFilling(); 1496 recalcShapeAndBoundingRect(); 1497 } 1498 1499 /*! 1500 recalculates the painter path for the drop lines. 1501 Called each time when the type of the drop lines is changed. 1502 */ 1503 void XYCurvePrivate::updateDropLines() { 1504 dropLinePath = QPainterPath(); 1505 if (dropLine->dropLineType() == XYCurve::DropLineType::NoDropLine) { 1506 recalcShapeAndBoundingRect(); 1507 return; 1508 } 1509 1510 // calculate the scene points to get the values for m_pointsVisible 1511 calculateScenePoints(); 1512 1513 // calculate drop lines 1514 QVector<QLineF> dlines; 1515 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1516 const double xMin = plot()->range(Dimension::X, cs->index(Dimension::X)).start(); 1517 const double yMin = plot()->range(Dimension::Y, cs->index(Dimension::Y)).start(); 1518 1519 int i = 0; 1520 switch (dropLine->dropLineType()) { 1521 case XYCurve::DropLineType::NoDropLine: 1522 break; 1523 case XYCurve::DropLineType::X: 1524 for (const auto& point : qAsConst(m_logicalPoints)) { 1525 if (!m_pointVisible.at(i++)) 1526 continue; 1527 dlines.append(QLineF(point, QPointF(point.x(), yMin))); 1528 } 1529 break; 1530 case XYCurve::DropLineType::Y: 1531 for (const auto& point : qAsConst(m_logicalPoints)) { 1532 if (!m_pointVisible.at(i++)) 1533 continue; 1534 dlines.append(QLineF(point, QPointF(xMin, point.y()))); 1535 } 1536 break; 1537 case XYCurve::DropLineType::XY: 1538 for (const auto& point : qAsConst(m_logicalPoints)) { 1539 if (!m_pointVisible.at(i++)) 1540 continue; 1541 dlines.append(QLineF(point, QPointF(point.x(), yMin))); 1542 dlines.append(QLineF(point, QPointF(xMin, point.y()))); 1543 } 1544 break; 1545 case XYCurve::DropLineType::XZeroBaseline: 1546 for (const auto& point : qAsConst(m_logicalPoints)) { 1547 if (!m_pointVisible.at(i++)) 1548 continue; 1549 dlines.append(QLineF(point, QPointF(point.x(), 0))); 1550 } 1551 break; 1552 case XYCurve::DropLineType::XMinBaseline: 1553 for (const auto& point : qAsConst(m_logicalPoints)) { 1554 if (!m_pointVisible.at(i++)) 1555 continue; 1556 dlines.append(QLineF(point, QPointF(point.x(), yColumn->minimum()))); 1557 } 1558 break; 1559 case XYCurve::DropLineType::XMaxBaseline: 1560 for (const auto& point : qAsConst(m_logicalPoints)) { 1561 if (!m_pointVisible.at(i++)) 1562 continue; 1563 dlines.append(QLineF(point, QPointF(point.x(), yColumn->maximum()))); 1564 } 1565 break; 1566 } 1567 1568 // map the drop lines to scene coordinates 1569 dlines = q->cSystem->mapLogicalToScene(dlines); 1570 1571 // new painter path for the drop lines 1572 for (const auto& line : qAsConst(dlines)) { 1573 dropLinePath.moveTo(line.p1()); 1574 dropLinePath.lineTo(line.p2()); 1575 } 1576 1577 recalcShapeAndBoundingRect(); 1578 } 1579 1580 void XYCurvePrivate::updateSymbols() { 1581 #if PERFTRACE_CURVES 1582 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QStringLiteral(", curve ") + name()); 1583 #endif 1584 symbolsPath = QPainterPath(); 1585 if (symbol->style() != Symbol::Style::NoSymbols) { 1586 QPainterPath path = Symbol::stylePath(symbol->style()); 1587 1588 QTransform trafo; 1589 trafo.scale(symbol->size(), symbol->size()); 1590 path = trafo.map(path); 1591 trafo.reset(); 1592 1593 if (symbol->rotationAngle() != 0.) { 1594 trafo.rotate(symbol->rotationAngle()); 1595 path = trafo.map(path); 1596 } 1597 calculateScenePoints(); 1598 for (const auto& point : qAsConst(m_scenePoints)) { 1599 trafo.reset(); 1600 trafo.translate(point.x(), point.y()); 1601 symbolsPath.addPath(trafo.map(path)); 1602 } 1603 } 1604 1605 recalcShapeAndBoundingRect(); 1606 } 1607 1608 void XYCurvePrivate::updateRug() { 1609 rugPath = QPainterPath(); 1610 1611 if (!rugEnabled || !plot()) { 1612 recalcShapeAndBoundingRect(); 1613 return; 1614 } 1615 1616 QVector<QPointF> points; 1617 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1618 const double xMin = plot()->range(Dimension::X, cs->index(Dimension::X)).start(); 1619 const double yMin = plot()->range(Dimension::Y, cs->index(Dimension::Y)).start(); 1620 1621 // vertical rug 1622 if (rugOrientation == WorksheetElement::Orientation::Vertical || rugOrientation == WorksheetElement::Orientation::Both) { 1623 for (const auto& point : qAsConst(m_logicalPoints)) 1624 points << QPointF(xMin, point.y()); 1625 1626 // map the points to scene coordinates 1627 points = q->cSystem->mapLogicalToScene(points); 1628 1629 // path for the vertical rug lines 1630 for (const auto& point : qAsConst(points)) { 1631 rugPath.moveTo(point.x() + rugOffset, point.y()); 1632 rugPath.lineTo(point.x() + rugOffset + rugLength, point.y()); 1633 } 1634 } 1635 1636 // horizontal rug 1637 if (rugOrientation == WorksheetElement::Orientation::Horizontal || rugOrientation == WorksheetElement::Orientation::Both) { 1638 points.clear(); 1639 for (const auto& point : qAsConst(m_logicalPoints)) 1640 points << QPointF(point.x(), yMin); 1641 1642 // map the points to scene coordinates 1643 points = q->cSystem->mapLogicalToScene(points); 1644 1645 // path for the horizontal rug lines 1646 for (const auto& point : qAsConst(points)) { 1647 rugPath.moveTo(point.x(), point.y() - rugOffset); 1648 rugPath.lineTo(point.x(), point.y() - rugOffset - rugLength); 1649 } 1650 } 1651 1652 recalcShapeAndBoundingRect(); 1653 } 1654 1655 /*! 1656 recreates the value strings to be shown and recalculates their draw position. 1657 */ 1658 void XYCurvePrivate::updateValues() { 1659 #if PERFTRACE_CURVES 1660 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QLatin1String(", curve ") + name()); 1661 #endif 1662 valuesPath = QPainterPath(); 1663 m_valuePoints.clear(); 1664 m_valueStrings.clear(); 1665 1666 const int numberOfPoints = m_logicalPoints.size(); 1667 if (valuesType == XYCurve::ValuesType::NoValues || numberOfPoints == 0) { 1668 recalcShapeAndBoundingRect(); 1669 return; 1670 } 1671 m_valuePoints.reserve(numberOfPoints); 1672 m_valueStrings.reserve(numberOfPoints); 1673 1674 calculateScenePoints(); 1675 1676 // determine the value string for all points that are currently visible in the plot 1677 int i{0}; 1678 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1679 const auto numberLocale = QLocale(); 1680 switch (valuesType) { 1681 case XYCurve::ValuesType::NoValues: 1682 case XYCurve::ValuesType::X: { 1683 auto xRangeFormat{plot()->range(Dimension::X, cs->index(Dimension::X)).format()}; 1684 int precision = valuesPrecision; 1685 if (xColumn->columnMode() == AbstractColumn::ColumnMode::Integer || xColumn->columnMode() == AbstractColumn::ColumnMode::BigInt) 1686 precision = 0; 1687 for (const auto& point : qAsConst(m_logicalPoints)) { 1688 if (!m_pointVisible.at(i++)) 1689 continue; 1690 QString value; 1691 if (xRangeFormat == RangeT::Format::Numeric) 1692 value = numberLocale.toString(point.x(), valuesNumericFormat, precision); 1693 else 1694 value = QDateTime::fromMSecsSinceEpoch(point.x(), Qt::UTC).toString(valuesDateTimeFormat); 1695 m_valueStrings << valuesPrefix + value + valuesSuffix; 1696 } 1697 break; 1698 } 1699 case XYCurve::ValuesType::Y: { 1700 auto rangeFormat{plot()->range(Dimension::Y, cs->index(Dimension::Y)).format()}; 1701 int precision = valuesPrecision; 1702 if (yColumn->columnMode() == AbstractColumn::ColumnMode::Integer || yColumn->columnMode() == AbstractColumn::ColumnMode::BigInt) 1703 precision = 0; 1704 for (const auto& point : qAsConst(m_logicalPoints)) { 1705 if (!m_pointVisible.at(i++)) 1706 continue; 1707 QString value; 1708 if (rangeFormat == RangeT::Format::Numeric) 1709 value = numberLocale.toString(point.y(), valuesNumericFormat, precision); 1710 else 1711 value = QDateTime::fromMSecsSinceEpoch(point.y(), Qt::UTC).toString(valuesDateTimeFormat); 1712 m_valueStrings << valuesPrefix + value + valuesSuffix; 1713 } 1714 break; 1715 } 1716 case XYCurve::ValuesType::XY: 1717 case XYCurve::ValuesType::XYBracketed: { 1718 auto xRangeFormat{plot()->range(Dimension::X, cs->index(Dimension::X)).format()}; 1719 auto yRangeFormat{plot()->range(Dimension::Y, cs->index(Dimension::Y)).format()}; 1720 1721 int xPrecision = valuesPrecision; 1722 if (xColumn->columnMode() == AbstractColumn::ColumnMode::Integer || xColumn->columnMode() == AbstractColumn::ColumnMode::BigInt) 1723 xPrecision = 0; 1724 1725 int yPrecision = valuesPrecision; 1726 if (yColumn->columnMode() == AbstractColumn::ColumnMode::Integer || yColumn->columnMode() == AbstractColumn::ColumnMode::BigInt) 1727 yPrecision = 0; 1728 1729 for (const auto& point : qAsConst(m_logicalPoints)) { 1730 if (!m_pointVisible.at(i++)) 1731 continue; 1732 QString value; 1733 if (valuesType == XYCurve::ValuesType::XYBracketed) 1734 value = QLatin1Char('('); 1735 if (xRangeFormat == RangeT::Format::Numeric) 1736 value += numberLocale.toString(point.x(), valuesNumericFormat, xPrecision); 1737 else 1738 value += QDateTime::fromMSecsSinceEpoch(point.x(), Qt::UTC).toString(valuesDateTimeFormat); 1739 1740 if (yRangeFormat == RangeT::Format::Numeric) 1741 value += QLatin1Char(',') + numberLocale.toString(point.y(), valuesNumericFormat, yPrecision); 1742 else 1743 value += QLatin1Char(',') + QDateTime::fromMSecsSinceEpoch(point.y(), Qt::UTC).toString(valuesDateTimeFormat); 1744 1745 if (valuesType == XYCurve::ValuesType::XYBracketed) 1746 value += QLatin1Char(')'); 1747 1748 m_valueStrings << valuesPrefix + value + valuesSuffix; 1749 } 1750 break; 1751 } 1752 case XYCurve::ValuesType::CustomColumn: { 1753 if (!valuesColumn) { 1754 recalcShapeAndBoundingRect(); 1755 return; 1756 } 1757 1758 const int endRow{std::min(std::min(xColumn->rowCount(), yColumn->rowCount()), valuesColumn->rowCount())}; 1759 auto xColMode{xColumn->columnMode()}; 1760 auto vColMode{valuesColumn->columnMode()}; 1761 1762 // need to check x range 1763 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1764 auto xRange = plot()->range(Dimension::X, cs->index(Dimension::X)); 1765 1766 size_t index = 0; // index of valid points (logicalPoints) 1767 for (int i = 0; i < endRow; ++i) { 1768 // ignore value labels for invalid data points 1769 // otherwise the assignment to the data points get lost 1770 if (!xColumn->isValid(i) || xColumn->isMasked(i) || !yColumn->isValid(i) || yColumn->isMasked(i) || !m_pointVisible.at(index++)) 1771 continue; 1772 1773 if (!valuesColumn->isValid(i) || valuesColumn->isMasked(i)) { 1774 m_valueStrings << QString(); 1775 continue; 1776 } 1777 1778 // check if inside x range 1779 switch (xColMode) { 1780 case AbstractColumn::ColumnMode::Double: 1781 case AbstractColumn::ColumnMode::Integer: 1782 case AbstractColumn::ColumnMode::BigInt: 1783 if (!xRange.contains(xColumn->valueAt(i))) 1784 continue; 1785 break; 1786 case AbstractColumn::ColumnMode::DateTime: 1787 case AbstractColumn::ColumnMode::Month: 1788 case AbstractColumn::ColumnMode::Day: 1789 if (xColumn->dateTimeAt(i) < QDateTime::fromMSecsSinceEpoch(xRange.start(), Qt::UTC) 1790 || xColumn->dateTimeAt(i) > QDateTime::fromMSecsSinceEpoch(xRange.end(), Qt::UTC)) 1791 continue; 1792 break; 1793 case AbstractColumn::ColumnMode::Text: 1794 break; 1795 } 1796 1797 switch (vColMode) { 1798 case AbstractColumn::ColumnMode::Double: 1799 m_valueStrings << valuesPrefix + numberLocale.toString(valuesColumn->valueAt(i), valuesNumericFormat, valuesPrecision) + valuesSuffix; 1800 break; 1801 case AbstractColumn::ColumnMode::Integer: 1802 case AbstractColumn::ColumnMode::BigInt: 1803 m_valueStrings << valuesPrefix + numberLocale.toString(valuesColumn->valueAt(i)) + valuesSuffix; 1804 break; 1805 case AbstractColumn::ColumnMode::Text: 1806 m_valueStrings << valuesPrefix + valuesColumn->textAt(i) + valuesSuffix; 1807 break; 1808 case AbstractColumn::ColumnMode::DateTime: 1809 case AbstractColumn::ColumnMode::Month: 1810 case AbstractColumn::ColumnMode::Day: 1811 m_valueStrings << valuesPrefix + valuesColumn->dateTimeAt(i).toString(valuesDateTimeFormat) + valuesSuffix; 1812 break; 1813 } 1814 } 1815 } 1816 } 1817 m_valueStrings.squeeze(); 1818 1819 // Calculate the coordinates where to paint the value strings. 1820 // The coordinates depend on the actual size of the string. 1821 QPointF tempPoint; 1822 QFontMetrics fm(valuesFont); 1823 const int h{fm.ascent()}; 1824 1825 i = 0; 1826 for (const auto& string : qAsConst(m_valueStrings)) { 1827 // catch case with more label strings than scene points (should not happen even with custom column) 1828 if (i >= m_scenePoints.size()) 1829 break; 1830 const int w{fm.boundingRect(string).width()}; 1831 const double x{m_scenePoints.at(i).x()}; 1832 const double y{m_scenePoints.at(i).y()}; 1833 i++; 1834 1835 switch (valuesPosition) { 1836 case XYCurve::ValuesPosition::Above: 1837 tempPoint = QPointF(x - w / 2., y - valuesDistance); 1838 break; 1839 case XYCurve::ValuesPosition::Under: 1840 tempPoint = QPointF(x - w / 2., y + valuesDistance + h / 2.); 1841 break; 1842 case XYCurve::ValuesPosition::Left: 1843 tempPoint = QPointF(x - valuesDistance - w - 1., y); 1844 break; 1845 case XYCurve::ValuesPosition::Right: 1846 tempPoint = QPointF(x + valuesDistance - 1., y); 1847 break; 1848 } 1849 m_valuePoints.append(tempPoint); 1850 } 1851 m_valuePoints.squeeze(); 1852 1853 QTransform trafo; 1854 QPainterPath path; 1855 i = 0; 1856 for (const auto& point : qAsConst(m_valuePoints)) { 1857 path = QPainterPath(); 1858 path.addText(QPoint(0, 0), valuesFont, m_valueStrings.at(i++)); 1859 1860 trafo.reset(); 1861 trafo.translate(point.x(), point.y()); 1862 if (valuesRotationAngle != 0) 1863 trafo.rotate(-valuesRotationAngle); 1864 1865 valuesPath.addPath(trafo.map(path)); 1866 } 1867 1868 recalcShapeAndBoundingRect(); 1869 } 1870 1871 void XYCurvePrivate::updateFilling() { 1872 if (suppressRetransform) 1873 return; 1874 1875 m_fillPolygons.clear(); 1876 1877 // don't try to calculate the filling polygons if 1878 // - no filling was enabled 1879 // - the number of visible points on the scene is too high 1880 // - no scene points available, everything outside of the plot region or no scene points calculated yet 1881 auto fillingPosition = background->position(); 1882 if (fillingPosition == Background::Position::No) { 1883 recalcShapeAndBoundingRect(); 1884 return; 1885 } 1886 calculateScenePoints(); // TODO: find other way 1887 if (m_scenePoints.size() > 1000 || m_scenePoints.isEmpty()) { 1888 recalcShapeAndBoundingRect(); 1889 return; 1890 } 1891 1892 QVector<QLineF> fillLines; 1893 1894 // if there're no interpolation lines available (XYCurve::NoLine selected), create line-interpolation, 1895 // use already available lines otherwise. 1896 if (!m_lines.isEmpty()) 1897 fillLines = m_lines; 1898 else { 1899 for (int i = 0; i < m_logicalPoints.size() - 1; i++) { 1900 if (!lineSkipGaps && !connectedPointsLogical[i]) 1901 continue; 1902 fillLines.append(QLineF(m_logicalPoints.at(i), m_logicalPoints.at(i + 1))); 1903 } 1904 1905 // no lines available (no points), nothing to do 1906 if (fillLines.isEmpty()) 1907 return; 1908 1909 fillLines = q->cSystem->mapLogicalToScene(fillLines); 1910 1911 // no lines available (no points) after mapping, nothing to do 1912 if (fillLines.isEmpty()) 1913 return; 1914 } 1915 1916 // create polygon(s): 1917 // 1. Depending on the current zoom-level, only a subset of the curve may be visible in the plot 1918 // and more of the filling area should be shown than the area defined by the start and end points of the currently visible points. 1919 // We check first whether the curve crosses the boundaries of the plot and determine new start and end points and put them to the boundaries. 1920 // 2. Furthermore, depending on the current filling type we determine the end point (x- or y-coordinate) where all polygons are closed at the end. 1921 QPolygonF pol; 1922 QPointF start = fillLines.at(0).p1(); // starting point of the current polygon, initialize with the first visible point 1923 QPointF end = fillLines.at(fillLines.size() - 1).p2(); // end point of the current polygon, initialize with the last visible point 1924 const QPointF& first = m_logicalPoints.at(0); // first point of the curve, may not be visible currently 1925 const QPointF& last = m_logicalPoints.at(m_logicalPoints.size() - 1); // last point of the curve, may not be visible currently 1926 QPointF edge; 1927 double xEnd{0.}, yEnd{0.}; 1928 auto cs = plot()->coordinateSystem(q->coordinateSystemIndex()); 1929 const auto xRange{plot()->range(Dimension::X, cs->index(Dimension::X))}; 1930 const auto yRange{plot()->range(Dimension::Y, cs->index(Dimension::Y))}; 1931 const double xMin{xRange.start()}, xMax{xRange.end()}; 1932 const double yMin{yRange.start()}, yMax{yRange.end()}; 1933 bool visible; 1934 if (fillingPosition == Background::Position::Above) { 1935 edge = q->cSystem->mapLogicalToScene(QPointF(xMin, yMin), visible); 1936 1937 // start point 1938 if (nsl_math_essentially_equal(start.y(), edge.y())) { 1939 if (first.x() < xMin) 1940 start = edge; 1941 else if (first.x() > xMax) 1942 start = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible); 1943 else 1944 start = q->cSystem->mapLogicalToScene(QPointF(first.x(), yMin), visible); 1945 } 1946 1947 // end point 1948 if (nsl_math_essentially_equal(end.y(), edge.y())) { 1949 if (last.x() < xMin) 1950 end = edge; 1951 else if (last.x() > xMax) 1952 end = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible); 1953 else 1954 end = q->cSystem->mapLogicalToScene(QPointF(last.x(), yMin), visible); 1955 } 1956 1957 // coordinate at which to close all polygons 1958 yEnd = q->cSystem->mapLogicalToScene(QPointF(xMin, yMax), visible).y(); 1959 } else if (fillingPosition == Background::Position::Below) { 1960 edge = q->cSystem->mapLogicalToScene(QPointF(xMin, yMax), visible); 1961 1962 // start point 1963 if (nsl_math_essentially_equal(start.y(), edge.y())) { 1964 if (first.x() < xMin) 1965 start = edge; 1966 else if (first.x() > xMax) 1967 start = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 1968 else 1969 start = q->cSystem->mapLogicalToScene(QPointF(first.x(), yMax), visible); 1970 } 1971 1972 // end point 1973 if (nsl_math_essentially_equal(end.y(), edge.y())) { 1974 if (last.x() < xMin) 1975 end = edge; 1976 else if (last.x() > xMax) 1977 end = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 1978 else 1979 end = q->cSystem->mapLogicalToScene(QPointF(last.x(), yMax), visible); 1980 } 1981 1982 // coordinate at which to close all polygons 1983 yEnd = q->cSystem->mapLogicalToScene(QPointF(xMin, yMin), visible).y(); 1984 } else if (fillingPosition == Background::Position::ZeroBaseline) { 1985 edge = q->cSystem->mapLogicalToScene(QPointF(xMin, yMax), visible); 1986 1987 // start point 1988 if (nsl_math_essentially_equal(start.y(), edge.y())) { 1989 if (yMax > 0) { 1990 if (first.x() < xMin) 1991 start = edge; 1992 else if (first.x() > xRange.end()) 1993 start = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 1994 else 1995 start = q->cSystem->mapLogicalToScene(QPointF(first.x(), yMax), visible); 1996 } else { 1997 if (first.x() < xMin) 1998 start = edge; 1999 else if (first.x() > xMax) 2000 start = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible); 2001 else 2002 start = q->cSystem->mapLogicalToScene(QPointF(first.x(), yMin), visible); 2003 } 2004 } 2005 2006 // end point 2007 if (nsl_math_essentially_equal(end.y(), edge.y())) { 2008 if (yMax > 0) { 2009 if (last.x() < xMin) 2010 end = edge; 2011 else if (last.x() > xMax) 2012 end = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 2013 else 2014 end = q->cSystem->mapLogicalToScene(QPointF(last.x(), yMax), visible); 2015 } else { 2016 if (last.x() < xMin) 2017 end = edge; 2018 else if (last.x() > xMax) 2019 end = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible); 2020 else 2021 end = q->cSystem->mapLogicalToScene(QPointF(last.x(), yMin), visible); 2022 } 2023 } 2024 2025 yEnd = q->cSystem->mapLogicalToScene(QPointF(xMin, yMin > 0 ? yMin : 0), visible).y(); 2026 } else if (fillingPosition == Background::Position::Left) { 2027 edge = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible); 2028 2029 // start point 2030 if (nsl_math_essentially_equal(start.x(), edge.x())) { 2031 if (first.y() < yMin) 2032 start = edge; 2033 else if (first.y() > yMax) 2034 start = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 2035 else 2036 start = q->cSystem->mapLogicalToScene(QPointF(xMax, first.y()), visible); 2037 } 2038 2039 // end point 2040 if (nsl_math_essentially_equal(end.x(), edge.x())) { 2041 if (last.y() < yMin) 2042 end = edge; 2043 else if (last.y() > yMax) 2044 end = q->cSystem->mapLogicalToScene(QPointF(xMax, yMax), visible); 2045 else 2046 end = q->cSystem->mapLogicalToScene(QPointF(xMax, last.y()), visible); 2047 } 2048 2049 // coordinate at which to close all polygons 2050 xEnd = q->cSystem->mapLogicalToScene(QPointF(xMin, yMin), visible).x(); 2051 } else { // FillingRight 2052 edge = q->cSystem->mapLogicalToScene(QPointF(xMin, yMin), visible); 2053 2054 // start point 2055 if (nsl_math_essentially_equal(start.x(), edge.x())) { 2056 if (first.y() < yMin) 2057 start = edge; 2058 else if (first.y() > yMax) 2059 start = q->cSystem->mapLogicalToScene(QPointF(xMin, yMax), visible); 2060 else 2061 start = q->cSystem->mapLogicalToScene(QPointF(xMin, first.y()), visible); 2062 } 2063 2064 // end point 2065 if (nsl_math_essentially_equal(end.x(), edge.x())) { 2066 if (last.y() < yMin) 2067 end = edge; 2068 else if (last.y() > yMax) 2069 end = q->cSystem->mapLogicalToScene(QPointF(xMin, yMax), visible); 2070 else 2071 end = q->cSystem->mapLogicalToScene(QPointF(xMin, last.y()), visible); 2072 } 2073 2074 // coordinate at which to close all polygons 2075 xEnd = q->cSystem->mapLogicalToScene(QPointF(xMax, yMin), visible).x(); 2076 } 2077 2078 if (start != fillLines.at(0).p1()) 2079 pol << start; 2080 2081 QPointF p1, p2; 2082 for (int i = 0; i < fillLines.size(); ++i) { 2083 const QLineF& line = fillLines.at(i); 2084 p1 = line.p1(); 2085 p2 = line.p2(); 2086 if (i != 0 && p1 != fillLines.at(i - 1).p2()) { 2087 // the first point of the current line is not equal to the last point of the previous line 2088 //->check whether we have a break in between. 2089 const bool gap = false; // TODO 2090 if (!gap) { 2091 //-> we have no break in the curve -> connect the points by a horizontal/vertical line 2092 pol << fillLines.at(i - 1).p2() << p1; 2093 } else { 2094 //-> we have a break in the curve -> close the polygon, add it to the polygon list and start a new polygon 2095 if (fillingPosition == Background::Position::Above || fillingPosition == Background::Position::Below 2096 || fillingPosition == Background::Position::ZeroBaseline) { 2097 pol << QPointF(fillLines.at(i - 1).p2().x(), yEnd); 2098 pol << QPointF(start.x(), yEnd); 2099 } else { 2100 pol << QPointF(xEnd, fillLines.at(i - 1).p2().y()); 2101 pol << QPointF(xEnd, start.y()); 2102 } 2103 2104 m_fillPolygons << pol; 2105 pol.clear(); 2106 start = p1; 2107 } 2108 } 2109 pol << p1 << p2; 2110 } 2111 2112 if (p2 != end) 2113 pol << end; 2114 2115 // close the last polygon 2116 if (fillingPosition == Background::Position::Above || fillingPosition == Background::Position::Below 2117 || fillingPosition == Background::Position::ZeroBaseline) { 2118 pol << QPointF(end.x(), yEnd); 2119 pol << QPointF(start.x(), yEnd); 2120 } else { 2121 pol << QPointF(xEnd, end.y()); 2122 pol << QPointF(xEnd, start.y()); 2123 } 2124 2125 m_fillPolygons << pol; 2126 recalcShapeAndBoundingRect(); 2127 } 2128 2129 /*! 2130 * Find y value which corresponds to a @p x . @p valueFound indicates, if value was found. 2131 * When monotonic increasing or decreasing a different algorithm will be used, which needs less steps (mean) (log_2(rowCount)) to find the value. 2132 * @param x 2133 * @param valueFound 2134 * @return 2135 */ 2136 double XYCurve::y(double x, bool& valueFound) const { 2137 if (!yColumn() || !xColumn()) { 2138 valueFound = false; 2139 return NAN; 2140 } 2141 2142 const int index = xColumn()->indexForValue(x); 2143 if (index < 0) { 2144 valueFound = false; 2145 return NAN; 2146 } 2147 2148 valueFound = true; 2149 if (yColumn()->isNumeric()) 2150 return yColumn()->valueAt(index); 2151 else { 2152 valueFound = false; 2153 return NAN; 2154 } 2155 } 2156 2157 /*! 2158 * @param x :value for which y should be found 2159 * @param valueFound: returns true if y value found, otherwise false 2160 * @param x_new: exact x value where y value is 2161 * @return y value from x value 2162 */ 2163 double XYCurve::y(double x, double& x_new, bool& valueFound) const { 2164 int index = xColumn()->indexForValue(x); 2165 if (index < 0) { 2166 valueFound = false; 2167 return NAN; 2168 } 2169 2170 AbstractColumn::ColumnMode xColumnMode = xColumn()->columnMode(); 2171 if (xColumn()->isNumeric()) 2172 x_new = xColumn()->valueAt(index); 2173 else if (xColumnMode == AbstractColumn::ColumnMode::DateTime || xColumnMode == AbstractColumn::ColumnMode::Day 2174 || xColumnMode == AbstractColumn::ColumnMode::Month) 2175 x_new = xColumn()->dateTimeAt(index).toMSecsSinceEpoch(); 2176 else { 2177 // any other type implemented 2178 valueFound = false; 2179 return NAN; 2180 } 2181 2182 valueFound = true; 2183 if (yColumn()->isNumeric()) 2184 return yColumn()->valueAt(index); 2185 else { 2186 valueFound = false; 2187 return NAN; 2188 } 2189 } 2190 2191 /*! 2192 * Find y DateTime which corresponds to a @p x . @p valueFound indicates, if value was found. 2193 * When monotonic increasing or decreasing a different algorithm will be used, which needs less steps (mean) (log_2(rowCount)) to find the value. 2194 * @param x 2195 * @param valueFound 2196 * @return Return found value 2197 */ 2198 QDateTime XYCurve::yDateTime(double x, bool& valueFound) const { 2199 if (!yColumn() || !xColumn()) { 2200 valueFound = false; 2201 return {}; 2202 } 2203 2204 auto yColumnMode = yColumn()->columnMode(); 2205 const int index = xColumn()->indexForValue(x); 2206 if (index < 0) { 2207 valueFound = false; 2208 return {}; 2209 } 2210 2211 valueFound = true; 2212 if (yColumnMode == AbstractColumn::ColumnMode::Day || yColumnMode == AbstractColumn::ColumnMode::Month 2213 || yColumnMode == AbstractColumn::ColumnMode::DateTime) 2214 return yColumn()->dateTimeAt(index); 2215 2216 valueFound = false; 2217 return {}; 2218 } 2219 2220 bool XYCurve::minMax(const Dimension dim, const Range<int>& indexRange, Range<double>& r, bool includeErrorBars) const { 2221 Q_D(const XYCurve); 2222 switch (dim) { 2223 case Dimension::X: 2224 return minMax(xColumn(), yColumn(), d->xErrorBar->type(), d->xErrorBar->plusColumn(), d->xErrorBar->minusColumn(), indexRange, r, includeErrorBars); 2225 case Dimension::Y: 2226 return minMax(yColumn(), xColumn(), d->yErrorBar->type(), d->yErrorBar->plusColumn(), d->yErrorBar->minusColumn(), indexRange, r, includeErrorBars); 2227 } 2228 return false; 2229 } 2230 2231 /*! 2232 * Calculates the minimum \p min and maximum \p max of a curve with optionally respecting the error bars 2233 * This function does not check if the values are out of range 2234 * \p indexMax is not included 2235 * \p column1 2236 * \p column2 2237 * \p errorType 2238 * \p errorPlusColumn 2239 * \p errorMinusColumn 2240 * \p indexRange [min, max] 2241 * \p min 2242 * \p max 2243 * \p includeErrorBars If true respect the error bars in the min/max calculation 2244 */ 2245 bool XYCurve::minMax(const AbstractColumn* column1, 2246 const AbstractColumn* column2, 2247 const ErrorBar::Type errorType, 2248 const AbstractColumn* errorPlusColumn, 2249 const AbstractColumn* errorMinusColumn, 2250 const Range<int>& indexRange, 2251 Range<double>& range, 2252 bool includeErrorBars) const { 2253 #ifdef PERFTRACE_AUTOSCALE 2254 PERFTRACE(name() + QLatin1String(Q_FUNC_INFO)); 2255 #endif 2256 // when property is increasing or decreasing there is a benefit in finding minimum and maximum 2257 // for property == AbstractColumn::Properties::No it must be iterated over all values so it does not matter if this function or the below one is used 2258 // if the property of the second column is not AbstractColumn::Properties::No means, that all values are valid and not masked 2259 // DEBUG(Q_FUNC_INFO << "\n, column 1 min/max = " << column1->minimum() << "/" << column1->maximum()) 2260 if ((!includeErrorBars || errorType == ErrorBar::Type::NoError) && column1->properties() != AbstractColumn::Properties::No && column2 2261 && column2->properties() != AbstractColumn::Properties::No) { 2262 auto min = column1->minimum(indexRange.start(), indexRange.end()); 2263 auto max = column1->maximum(indexRange.start(), indexRange.end()); 2264 DEBUG(Q_FUNC_INFO << "\n, column 1 min/max in index range = " << min << "/" << max) 2265 // TODO: Range 2266 range.setRange(min, max); 2267 return true; 2268 } 2269 2270 if (column1->rowCount() == 0) 2271 return false; 2272 2273 range.setRange(INFINITY, -INFINITY); 2274 // DEBUG(Q_FUNC_INFO << ", calculate range for index range " << indexRange.start() << " .. " << indexRange.end()) 2275 2276 for (int i = indexRange.start(); i <= indexRange.end(); ++i) { 2277 if (!column1->isValid(i) || column1->isMasked(i) || (column2 && (!column2->isValid(i) || column2->isMasked(i)))) 2278 continue; 2279 2280 if ((errorPlusColumn && i >= errorPlusColumn->rowCount()) || (errorMinusColumn && i >= errorMinusColumn->rowCount())) 2281 continue; 2282 2283 double value; 2284 if (column1->isNumeric()) 2285 value = column1->valueAt(i); 2286 else if (column1->columnMode() == AbstractColumn::ColumnMode::DateTime || column1->columnMode() == AbstractColumn::ColumnMode::Month 2287 || column1->columnMode() == AbstractColumn::ColumnMode::Day) 2288 value = column1->dateTimeAt(i).toMSecsSinceEpoch(); 2289 else 2290 return false; 2291 2292 if (errorType == ErrorBar::Type::NoError) { 2293 if (value < range.start()) 2294 range.start() = value; 2295 2296 if (value > range.end()) 2297 range.end() = value; 2298 } else { 2299 // determine the values for the errors 2300 double errorPlus, errorMinus; 2301 if (errorPlusColumn && errorPlusColumn->isValid(i) && !errorPlusColumn->isMasked(i)) 2302 if (errorPlusColumn->isNumeric()) 2303 errorPlus = errorPlusColumn->valueAt(i); 2304 else if (errorPlusColumn->columnMode() == AbstractColumn::ColumnMode::DateTime 2305 || errorPlusColumn->columnMode() == AbstractColumn::ColumnMode::Month 2306 || errorPlusColumn->columnMode() == AbstractColumn::ColumnMode::Day) 2307 errorPlus = errorPlusColumn->dateTimeAt(i).toMSecsSinceEpoch(); 2308 else 2309 return false; 2310 else 2311 errorPlus = 0; 2312 2313 if (errorType == ErrorBar::Type::Symmetric) 2314 errorMinus = errorPlus; 2315 else { 2316 if (errorMinusColumn && errorMinusColumn->isValid(i) && !errorMinusColumn->isMasked(i)) 2317 if (errorMinusColumn->isNumeric()) 2318 errorMinus = errorMinusColumn->valueAt(i); 2319 else if (errorMinusColumn->columnMode() == AbstractColumn::ColumnMode::DateTime 2320 || errorMinusColumn->columnMode() == AbstractColumn::ColumnMode::Month 2321 || errorMinusColumn->columnMode() == AbstractColumn::ColumnMode::Day) 2322 errorMinus = errorMinusColumn->dateTimeAt(i).toMSecsSinceEpoch(); 2323 else 2324 return false; 2325 else 2326 errorMinus = 0; 2327 } 2328 2329 if (value - errorMinus < range.start()) 2330 range.start() = value - errorMinus; 2331 2332 if (value + errorPlus > range.end()) 2333 range.end() = value + errorPlus; 2334 } 2335 // DEBUG(Q_FUNC_INFO << ", range = " << range.toStdString()) 2336 } 2337 return true; 2338 } 2339 2340 bool XYCurvePrivate::activatePlot(QPointF mouseScenePos, double maxDist) { 2341 if (!isVisible()) 2342 return false; 2343 2344 int rowCount{0}; 2345 if (lineType != XYCurve::LineType::NoLine) 2346 rowCount = m_lines.count(); 2347 else if (symbol->style() != Symbol::Style::NoSymbols) { 2348 calculateScenePoints(); 2349 rowCount = m_scenePoints.size(); 2350 } else 2351 return false; 2352 2353 if (rowCount == 0) 2354 return false; 2355 2356 if (maxDist < 0) 2357 maxDist = (line->pen().width() < 10) ? 10. : line->pen().width(); 2358 2359 const double maxDistSquare = gsl_pow_2(maxDist); 2360 2361 auto properties = q->xColumn()->properties(); 2362 if (properties == AbstractColumn::Properties::No || properties == AbstractColumn::Properties::NonMonotonic) { 2363 // assumption: points exist if no line. otherwise previously returned false 2364 if (lineType == XYCurve::LineType::NoLine) { 2365 calculateScenePoints(); 2366 QPointF curvePosPrevScene = m_scenePoints.at(0); 2367 QPointF curvePosScene = curvePosPrevScene; 2368 for (int row = 0; row < rowCount; row++) { 2369 if (gsl_pow_2(mouseScenePos.x() - curvePosScene.x()) + gsl_pow_2(mouseScenePos.y() - curvePosScene.y()) <= maxDistSquare) 2370 return true; 2371 2372 curvePosPrevScene = curvePosScene; 2373 curvePosScene = m_scenePoints.at(row); 2374 } 2375 } else { 2376 for (int row = 0; row < rowCount; row++) { 2377 QLineF line = m_lines.at(row); 2378 if (pointLiesNearLine(line.p1(), line.p2(), mouseScenePos, maxDist)) 2379 return true; 2380 } 2381 } 2382 2383 } else if (properties == AbstractColumn::Properties::MonotonicIncreasing || properties == AbstractColumn::Properties::MonotonicDecreasing) { 2384 bool increase{true}; 2385 if (properties == AbstractColumn::Properties::MonotonicDecreasing) 2386 increase = false; 2387 2388 double x{mouseScenePos.x() - maxDist}; 2389 int index{0}; 2390 2391 QPointF curvePosScene; 2392 QPointF curvePosPrevScene; 2393 2394 if (lineType == XYCurve::LineType::NoLine) { 2395 calculateScenePoints(); 2396 curvePosScene = m_scenePoints.at(index); 2397 curvePosPrevScene = curvePosScene; 2398 index = Column::indexForValue(x, m_scenePoints, static_cast<AbstractColumn::Properties>(properties)); 2399 } else 2400 index = Column::indexForValue(x, m_lines, static_cast<AbstractColumn::Properties>(properties)); 2401 2402 if (index >= 1) 2403 index--; // use one before so it is secured that I'm before point.x() 2404 else if (index == -1) 2405 return false; 2406 2407 const double xMax{mouseScenePos.x() + maxDist}; 2408 bool stop{false}; 2409 while (true) { 2410 // assumption: points exist if no line. otherwise previously returned false 2411 if (lineType == XYCurve::LineType::NoLine) { // check points only if no line otherwise check only the lines 2412 if (curvePosScene.x() > xMax) 2413 stop = true; // one more time if bigger 2414 if (gsl_hypot(mouseScenePos.x() - curvePosScene.x(), mouseScenePos.y() - curvePosScene.y()) <= maxDist) 2415 return true; 2416 } else { 2417 if (m_lines.at(index).p1().x() > xMax) 2418 stop = true; // one more time if bigger 2419 2420 QLineF line = m_lines.at(index); 2421 if (pointLiesNearLine(line.p1(), line.p2(), mouseScenePos, maxDist)) 2422 return true; 2423 } 2424 2425 if (stop || (index >= rowCount - 1 && increase) || (index <= 0 && !increase)) 2426 break; 2427 2428 if (increase) 2429 index++; 2430 else 2431 index--; 2432 2433 if (lineType == XYCurve::LineType::NoLine) { 2434 calculateScenePoints(); 2435 curvePosPrevScene = curvePosScene; 2436 curvePosScene = m_scenePoints.at(index); 2437 } 2438 } 2439 } 2440 2441 return false; 2442 } 2443 2444 /*! 2445 * \brief XYCurve::pointLiesNearLine 2446 * Calculates if a point \p pos lies near than maxDist to the line created by the points \p p1 and \p p2 2447 * https://stackoverflow.com/questions/11604680/point-laying-near-line 2448 * \p p1 first point of the line 2449 * \p p2 second point of the line 2450 * \p pos Position to check 2451 * \p maxDist Maximal distance away from the curve, which is valid 2452 * \return Return true if point lies next to the line 2453 */ 2454 bool XYCurvePrivate::pointLiesNearLine(const QPointF p1, const QPointF p2, const QPointF pos, const double maxDist) const { 2455 const double dx12{p2.x() - p1.x()}; 2456 const double dy12{p2.y() - p1.y()}; 2457 const double vecLength{gsl_hypot(dx12, dy12)}; 2458 2459 const double dx1m{pos.x() - p1.x()}; 2460 const double dy1m{pos.y() - p1.y()}; 2461 if (vecLength == 0.) { 2462 if (gsl_hypot(dx1m, dy1m) <= maxDist) 2463 return true; 2464 return false; 2465 } 2466 QPointF unitvec(dx12 / vecLength, dy12 / vecLength); 2467 2468 const double dist_segm{std::abs(dx1m * unitvec.y() - dy1m * unitvec.x())}; 2469 const double scalarProduct{dx1m * unitvec.x() + dy1m * unitvec.y()}; 2470 2471 if (scalarProduct > 0) { 2472 if (scalarProduct < vecLength && dist_segm < maxDist) 2473 return true; 2474 } 2475 return false; 2476 } 2477 2478 // TODO: curvePosScene.x() >= mouseScenePos.x() && 2479 // curvePosPrevScene.x() < mouseScenePos.x() 2480 // should not be here 2481 bool XYCurvePrivate::pointLiesNearCurve(const QPointF mouseScenePos, 2482 const QPointF curvePosPrevScene, 2483 const QPointF curvePosScene, 2484 const int index, 2485 const double maxDist) const { 2486 if (q->lineType() != XYCurve::LineType::NoLine && curvePosScene.x() >= mouseScenePos.x() && curvePosPrevScene.x() < mouseScenePos.x()) { 2487 if (q->lineType() == XYCurve::LineType::Line) { 2488 // point is not in the near of the point, but it can be in the near of the connection line of two points 2489 if (pointLiesNearLine(curvePosPrevScene, curvePosScene, mouseScenePos, maxDist)) 2490 return true; 2491 } else if (q->lineType() == XYCurve::LineType::StartHorizontal) { 2492 QPointF tempPoint = curvePosPrevScene; 2493 tempPoint.setX(curvePosScene.x()); 2494 if (pointLiesNearLine(curvePosPrevScene, tempPoint, mouseScenePos, maxDist)) 2495 return true; 2496 if (pointLiesNearLine(tempPoint, curvePosScene, mouseScenePos, maxDist)) 2497 return true; 2498 } else if (q->lineType() == XYCurve::LineType::StartVertical) { 2499 QPointF tempPoint = curvePosPrevScene; 2500 tempPoint.setY(curvePosScene.y()); 2501 if (pointLiesNearLine(curvePosPrevScene, tempPoint, mouseScenePos, maxDist)) 2502 return true; 2503 if (pointLiesNearLine(tempPoint, curvePosScene, mouseScenePos, maxDist)) 2504 return true; 2505 } else if (q->lineType() == XYCurve::LineType::MidpointHorizontal) { 2506 QPointF tempPoint = curvePosPrevScene; 2507 tempPoint.setX(curvePosPrevScene.x() + (curvePosScene.x() - curvePosPrevScene.x()) / 2); 2508 if (pointLiesNearLine(curvePosPrevScene, tempPoint, mouseScenePos, maxDist)) 2509 return true; 2510 QPointF tempPoint2(tempPoint.x(), curvePosScene.y()); 2511 if (pointLiesNearLine(tempPoint, tempPoint2, mouseScenePos, maxDist)) 2512 return true; 2513 2514 if (pointLiesNearLine(tempPoint2, curvePosScene, mouseScenePos, maxDist)) 2515 return true; 2516 } else if (q->lineType() == XYCurve::LineType::MidpointVertical) { 2517 QPointF tempPoint = curvePosPrevScene; 2518 tempPoint.setY(curvePosPrevScene.y() + (curvePosScene.y() - curvePosPrevScene.y()) / 2); 2519 if (pointLiesNearLine(curvePosPrevScene, tempPoint, mouseScenePos, maxDist)) 2520 return true; 2521 QPointF tempPoint2(tempPoint.y(), curvePosScene.x()); 2522 if (pointLiesNearLine(tempPoint, tempPoint2, mouseScenePos, maxDist)) 2523 return true; 2524 2525 if (pointLiesNearLine(tempPoint2, curvePosScene, mouseScenePos, maxDist)) 2526 return true; 2527 } else if (q->lineType() == XYCurve::LineType::SplineAkimaNatural || q->lineType() == XYCurve::LineType::SplineCubicNatural 2528 || q->lineType() == XYCurve::LineType::SplineAkimaPeriodic || q->lineType() == XYCurve::LineType::SplineCubicPeriodic) { 2529 for (int i = 0; i < q->lineInterpolationPointsCount() + 1; i++) { 2530 QLineF line = m_lines.at(index * (q->lineInterpolationPointsCount() + 1) + i); 2531 QPointF p1{line.p1()}; // cSystem->mapLogicalToScene(line.p1()); 2532 QPointF p2{line.p2()}; // cSystem->mapLogicalToScene(line.p2()); 2533 if (pointLiesNearLine(p1, p2, mouseScenePos, maxDist)) 2534 return true; 2535 } 2536 } else { 2537 // point is not in the near of the point, but it can be in the near of the connection line of two points 2538 if (pointLiesNearLine(curvePosPrevScene, curvePosScene, mouseScenePos, maxDist)) 2539 return true; 2540 } 2541 } 2542 return false; 2543 } 2544 2545 void XYCurvePrivate::updateErrorBars() { 2546 errorBarsPath = QPainterPath(); 2547 if (xErrorBar->type() == ErrorBar::Type::NoError && yErrorBar->type() == ErrorBar::Type::NoError) { 2548 recalcShapeAndBoundingRect(); 2549 return; 2550 } 2551 2552 QVector<QLineF> elines; 2553 QVector<QPointF> pointsErrorBarAnchorX; 2554 QVector<QPointF> pointsErrorBarAnchorY; 2555 const auto errorBarStyleType = errorBarStyle->type(); 2556 2557 calculateScenePoints(); 2558 2559 for (int i = 0; i < m_logicalPoints.size(); ++i) { 2560 if (!m_pointVisible.at(i)) 2561 continue; 2562 2563 const QPointF& point{m_logicalPoints.at(i)}; 2564 const int index{validPointsIndicesLogical.at(i)}; 2565 double errorPlus, errorMinus; 2566 2567 // error bars for x 2568 const auto xErrorType = xErrorBar->type(); 2569 const auto* xErrorPlusColumn = xErrorBar->plusColumn(); 2570 const auto* xErrorMinusColumn = xErrorBar->minusColumn(); 2571 if (xErrorType != ErrorBar::Type::NoError) { 2572 // determine the values for the errors 2573 if (xErrorPlusColumn && xErrorPlusColumn->isValid(index) && !xErrorPlusColumn->isMasked(index)) 2574 errorPlus = xErrorPlusColumn->valueAt(index); 2575 else 2576 errorPlus = 0; 2577 2578 if (xErrorType == ErrorBar::Type::Symmetric) 2579 errorMinus = errorPlus; 2580 else { 2581 if (xErrorMinusColumn && xErrorMinusColumn->isValid(index) && !xErrorMinusColumn->isMasked(index)) 2582 errorMinus = xErrorMinusColumn->valueAt(index); 2583 else 2584 errorMinus = 0; 2585 } 2586 2587 // draw the error bars 2588 if (errorMinus != 0. || errorPlus != 0.) 2589 elines.append(QLineF(QPointF(point.x() - errorMinus, point.y()), QPointF(point.x() + errorPlus, point.y()))); 2590 2591 // determine the end points of the errors bars in logical coordinates to draw later the cap 2592 if (errorBarStyleType == ErrorBarStyle::Type::WithEnds) { 2593 if (errorMinus != 0.) 2594 pointsErrorBarAnchorX << QPointF(point.x() - errorMinus, point.y()); 2595 if (errorPlus != 0.) 2596 pointsErrorBarAnchorX << QPointF(point.x() + errorPlus, point.y()); 2597 } 2598 } 2599 2600 // error bars for y 2601 const auto yErrorType = yErrorBar->type(); 2602 const auto* yErrorPlusColumn = yErrorBar->plusColumn(); 2603 const auto* yErrorMinusColumn = yErrorBar->minusColumn(); 2604 if (yErrorType != ErrorBar::Type::NoError) { 2605 // determine the values for the errors 2606 if (yErrorPlusColumn && yErrorPlusColumn->isValid(index) && !yErrorPlusColumn->isMasked(index)) 2607 errorPlus = yErrorPlusColumn->valueAt(index); 2608 else 2609 errorPlus = 0; 2610 2611 if (yErrorType == ErrorBar::Type::Symmetric) 2612 errorMinus = errorPlus; 2613 else { 2614 if (yErrorMinusColumn && yErrorMinusColumn->isValid(index) && !yErrorMinusColumn->isMasked(index)) 2615 errorMinus = yErrorMinusColumn->valueAt(index); 2616 else 2617 errorMinus = 0; 2618 } 2619 2620 // draw the error bars 2621 if (errorMinus != 0. || errorPlus != 0.) 2622 elines.append(QLineF(QPointF(point.x(), point.y() + errorPlus), QPointF(point.x(), point.y() - errorMinus))); 2623 2624 // determine the end points of the errors bars in logical coordinates to draw later the cap 2625 if (errorBarStyleType == ErrorBarStyle::Type::WithEnds) { 2626 if (errorMinus != 0.) 2627 pointsErrorBarAnchorY << QPointF(point.x(), point.y() + errorPlus); 2628 if (errorPlus != 0.) 2629 pointsErrorBarAnchorY << QPointF(point.x(), point.y() - errorMinus); 2630 } 2631 } 2632 } 2633 2634 // map the error bars to scene coordinates 2635 elines = q->cSystem->mapLogicalToScene(elines); 2636 2637 // new painter path for the error bars 2638 for (const auto& line : qAsConst(elines)) { 2639 errorBarsPath.moveTo(line.p1()); 2640 errorBarsPath.lineTo(line.p2()); 2641 } 2642 2643 // add caps for x error bars 2644 const auto errorBarsCapSize = errorBarStyle->capSize(); 2645 if (!pointsErrorBarAnchorX.isEmpty()) { 2646 pointsErrorBarAnchorX = q->cSystem->mapLogicalToScene(pointsErrorBarAnchorX); 2647 for (const auto& point : qAsConst(pointsErrorBarAnchorX)) { 2648 errorBarsPath.moveTo(QPointF(point.x(), point.y() - errorBarsCapSize / 2.)); 2649 errorBarsPath.lineTo(QPointF(point.x(), point.y() + errorBarsCapSize / 2.)); 2650 } 2651 } 2652 2653 // add caps for y error bars 2654 if (!pointsErrorBarAnchorY.isEmpty()) { 2655 pointsErrorBarAnchorY = q->cSystem->mapLogicalToScene(pointsErrorBarAnchorY); 2656 for (const auto& point : qAsConst(pointsErrorBarAnchorY)) { 2657 errorBarsPath.moveTo(QPointF(point.x() - errorBarsCapSize / 2., point.y())); 2658 errorBarsPath.lineTo(QPointF(point.x() + errorBarsCapSize / 2., point.y())); 2659 } 2660 } 2661 2662 recalcShapeAndBoundingRect(); 2663 } 2664 2665 /*! 2666 recalculates the outer bounds and the shape of the curve. 2667 */ 2668 void XYCurvePrivate::recalcShapeAndBoundingRect() { 2669 DEBUG(Q_FUNC_INFO << ", suppressRecalc = " << suppressRecalc); 2670 if (suppressRecalc) 2671 return; 2672 2673 #if PERFTRACE_CURVES 2674 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QLatin1String(", curve ") + name()); 2675 #endif 2676 2677 prepareGeometryChange(); 2678 m_shape = QPainterPath(); 2679 if (lineType != XYCurve::LineType::NoLine) 2680 m_shape.addPath(WorksheetElement::shapeFromPath(linePath, line->pen())); 2681 2682 if (dropLine->dropLineType() != XYCurve::DropLineType::NoDropLine) 2683 m_shape.addPath(WorksheetElement::shapeFromPath(dropLinePath, dropLine->pen())); 2684 2685 if (symbol->style() != Symbol::Style::NoSymbols) 2686 m_shape.addPath(symbolsPath); 2687 2688 m_shape.addPath(rugPath); 2689 2690 if (valuesType != XYCurve::ValuesType::NoValues) 2691 m_shape.addPath(valuesPath); 2692 2693 if (xErrorBar->type() != ErrorBar::Type::NoError || yErrorBar->type() != ErrorBar::Type::NoError) 2694 m_shape.addPath(WorksheetElement::shapeFromPath(errorBarsPath, errorBarStyle->line()->pen())); 2695 2696 m_boundingRectangle = m_shape.boundingRect(); 2697 2698 for (const auto& pol : qAsConst(m_fillPolygons)) 2699 m_boundingRectangle = m_boundingRectangle.united(pol.boundingRect()); 2700 2701 // TODO: when the selection is painted, line intersections are visible. 2702 // simplified() removes those artifacts but is horrible slow for curves with large number of points. 2703 // search for an alternative. 2704 // m_shape = m_shape.simplified(); 2705 2706 updatePixmap(); 2707 } 2708 2709 void XYCurvePrivate::draw(QPainter* painter) { 2710 #if PERFTRACE_CURVES 2711 PERFTRACE(QLatin1String(Q_FUNC_INFO) + QLatin1String(", curve ") + name()); 2712 #endif 2713 2714 // draw filling 2715 if (background->position() != Background::Position::No) { 2716 painter->setOpacity(background->opacity()); 2717 painter->setPen(Qt::SolidLine); 2718 for (const auto& polygon : qAsConst(m_fillPolygons)) 2719 drawFillingPollygon(polygon, painter, background); 2720 } 2721 2722 // draw lines 2723 if (lineType != XYCurve::LineType::NoLine) { 2724 painter->setOpacity(line->opacity()); 2725 painter->setPen(line->pen()); 2726 painter->setBrush(Qt::NoBrush); 2727 if (line->pen().style() == Qt::SolidLine && !q->isPrinting()) { 2728 // Much fast than drawPath but has problems 2729 // with different styles 2730 // When exporting to svg or pdf, this creates for every line 2731 // it's own path in the saved file which is not desired. We 2732 // would like to have one complete path for a curve not many paths 2733 for (auto& line : m_lines) 2734 painter->drawLine(line); 2735 } else { 2736 painter->drawPath(linePath); 2737 } 2738 } 2739 2740 // draw drop lines 2741 if (dropLine->dropLineType() != XYCurve::DropLineType::NoDropLine) { 2742 painter->setOpacity(dropLine->opacity()); 2743 painter->setPen(dropLine->pen()); 2744 painter->setBrush(Qt::NoBrush); 2745 painter->drawPath(dropLinePath); 2746 } 2747 2748 // draw error bars 2749 if ((xErrorBar->type() != ErrorBar::Type::NoError) || (yErrorBar->type() != ErrorBar::Type::NoError)) { 2750 painter->setOpacity(errorBarStyle->line()->opacity()); 2751 painter->setPen(errorBarStyle->line()->pen()); 2752 painter->setBrush(Qt::NoBrush); 2753 painter->drawPath(errorBarsPath); 2754 } 2755 2756 // draw symbols 2757 if (symbol->style() != Symbol::Style::NoSymbols) { 2758 calculateScenePoints(); 2759 symbol->draw(painter, m_scenePoints); 2760 } 2761 2762 // draw values 2763 if (valuesType != XYCurve::ValuesType::NoValues) { 2764 painter->setOpacity(valuesOpacity); 2765 painter->setPen(QPen(valuesColor)); 2766 painter->setFont(valuesFont); 2767 drawValues(painter); 2768 } 2769 2770 // draw rug 2771 if (rugEnabled) { 2772 QPen pen; 2773 pen.setColor(symbol->brush().color()); 2774 pen.setWidthF(rugWidth); 2775 painter->setPen(pen); 2776 painter->setOpacity(symbol->opacity()); 2777 painter->drawPath(rugPath); 2778 } 2779 } 2780 2781 void XYCurvePrivate::updatePixmap() { 2782 DEBUG(Q_FUNC_INFO << ", suppressRecalc = " << suppressRecalc); 2783 if (suppressRecalc) 2784 return; 2785 2786 m_hoverEffectImageIsDirty = true; 2787 m_selectionEffectImageIsDirty = true; 2788 if (m_boundingRectangle.width() == 0 || m_boundingRectangle.height() == 0) { 2789 DEBUG(Q_FUNC_INFO << ", boundingRectangle.width() or boundingRectangle.height() == 0"); 2790 m_pixmap = QPixmap(); 2791 return; 2792 } 2793 QPixmap pixmap(ceil(m_boundingRectangle.width()), ceil(m_boundingRectangle.height())); 2794 pixmap.fill(Qt::transparent); 2795 QPainter painter(&pixmap); 2796 painter.setRenderHint(QPainter::Antialiasing, true); 2797 painter.translate(-m_boundingRectangle.topLeft()); 2798 2799 draw(&painter); 2800 painter.end(); 2801 m_pixmap = pixmap; 2802 2803 update(); 2804 Q_EMIT q->changed(); 2805 } 2806 2807 QVariant XYCurvePrivate::itemChange(GraphicsItemChange change, const QVariant& value) { 2808 // signalize, that the curve was selected. Will be used to create a new InfoElement (Marker) 2809 if (change == QGraphicsItem::ItemSelectedChange) { 2810 if (value.toBool() && q->cSystem && q->cSystem->isValid()) { 2811 Q_EMIT q->selected(q->cSystem->mapSceneToLogical(mousePos).x()); 2812 } 2813 } 2814 return QGraphicsItem::itemChange(change, value); 2815 } 2816 2817 /*! 2818 Reimplementation of QGraphicsItem::paint(). This function does the actual painting of the curve. 2819 \sa QGraphicsItem::paint(). 2820 */ 2821 void XYCurvePrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) { 2822 if (!isVisible()) 2823 return; 2824 2825 painter->setPen(Qt::NoPen); 2826 painter->setBrush(Qt::NoBrush); 2827 painter->setRenderHint(QPainter::SmoothPixmapTransform, true); 2828 2829 if (!q->isPrinting() && Settings::group(QStringLiteral("Settings_Worksheet")).readEntry<bool>("DoubleBuffering", true)) 2830 painter->drawPixmap(m_boundingRectangle.topLeft(), m_pixmap); // draw the cached pixmap (fast) 2831 else 2832 draw(painter); // draw directly again (slow) 2833 2834 if (isHovered() && !isSelected() && !q->isPrinting()) { 2835 if (m_hoverEffectImageIsDirty) { 2836 QPixmap pix = m_pixmap; 2837 QPainter p(&pix); 2838 p.setCompositionMode(QPainter::CompositionMode_SourceIn); // source (shadow) pixels merged with the alpha channel of the destination (m_pixmap) 2839 p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Shadow)); 2840 p.end(); 2841 2842 m_hoverEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); 2843 m_hoverEffectImageIsDirty = false; 2844 } 2845 2846 painter->drawImage(m_boundingRectangle.topLeft(), m_hoverEffectImage, m_pixmap.rect()); 2847 return; 2848 } 2849 2850 if (isSelected() && !q->isPrinting()) { 2851 if (m_selectionEffectImageIsDirty) { 2852 QPixmap pix = m_pixmap; 2853 QPainter p(&pix); 2854 p.setCompositionMode(QPainter::CompositionMode_SourceIn); 2855 p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Highlight)); 2856 p.end(); 2857 2858 m_selectionEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); 2859 m_selectionEffectImageIsDirty = false; 2860 } 2861 2862 painter->drawImage(m_boundingRectangle.topLeft(), m_selectionEffectImage, m_pixmap.rect()); 2863 } 2864 } 2865 2866 void XYCurvePrivate::drawValues(QPainter* painter) { 2867 // QDEBUG(Q_FUNC_INFO << ", value strings = " << m_valueStrings) 2868 int i = 0; 2869 for (const auto& point : qAsConst(m_valuePoints)) { 2870 painter->translate(point); 2871 if (valuesRotationAngle != 0.) 2872 painter->rotate(-valuesRotationAngle); 2873 2874 painter->drawText(QPoint(0, 0), m_valueStrings.at(i++)); 2875 2876 if (valuesRotationAngle != 0.) 2877 painter->rotate(valuesRotationAngle); 2878 painter->translate(-point); 2879 } 2880 } 2881 2882 /*! 2883 * checks if the mousePress event was done near the histogram shape 2884 * and selects the graphics item if it is the case. 2885 * \p event 2886 */ 2887 void XYCurvePrivate::mousePressEvent(QGraphicsSceneMouseEvent* event) { 2888 if (plot()->mouseMode() != CartesianPlot::MouseMode::Selection) { 2889 event->ignore(); 2890 return QGraphicsItem::mousePressEvent(event); 2891 } 2892 mousePos = event->pos(); 2893 2894 if (q->activatePlot(event->pos())) { 2895 setSelected(true); 2896 return; 2897 } 2898 2899 event->ignore(); 2900 setSelected(false); 2901 QGraphicsItem::mousePressEvent(event); 2902 } 2903 2904 // ############################################################################## 2905 // ################## Serialization/Deserialization ########################### 2906 // ############################################################################## 2907 //! Save as XML 2908 void XYCurve::save(QXmlStreamWriter* writer) const { 2909 Q_D(const XYCurve); 2910 2911 writer->writeStartElement(QStringLiteral("xyCurve")); 2912 writeBasicAttributes(writer); 2913 writeCommentElement(writer); 2914 2915 // general 2916 writer->writeStartElement(QStringLiteral("general")); 2917 2918 // if the data columns are valid, write their current paths. 2919 // if not, write the last used paths so the columns can be restored later 2920 // when the columns with the same path are added again to the project 2921 if (d->xColumn) 2922 writer->writeAttribute(QStringLiteral("xColumn"), d->xColumn->path()); 2923 else 2924 writer->writeAttribute(QStringLiteral("xColumn"), d->xColumnPath); 2925 2926 if (d->yColumn) 2927 writer->writeAttribute(QStringLiteral("yColumn"), d->yColumn->path()); 2928 else 2929 writer->writeAttribute(QStringLiteral("yColumn"), d->yColumnPath); 2930 2931 writer->writeAttribute(QStringLiteral("plotRangeIndex"), QString::number(m_cSystemIndex)); 2932 writer->writeAttribute(QStringLiteral("legendVisible"), QString::number(d->legendVisible)); 2933 writer->writeAttribute(QStringLiteral("visible"), QString::number(d->isVisible())); 2934 writer->writeEndElement(); 2935 2936 // Line 2937 writer->writeStartElement(QStringLiteral("lines")); 2938 writer->writeAttribute(QStringLiteral("type"), QString::number(static_cast<int>(d->lineType))); 2939 writer->writeAttribute(QStringLiteral("skipGaps"), QString::number(d->lineSkipGaps)); 2940 writer->writeAttribute(QStringLiteral("increasingXOnly"), QString::number(d->lineIncreasingXOnly)); 2941 writer->writeAttribute(QStringLiteral("interpolationPointsCount"), QString::number(d->lineInterpolationPointsCount)); 2942 d->line->save(writer); 2943 writer->writeEndElement(); 2944 2945 // Drop lines 2946 d->dropLine->save(writer); 2947 2948 // Symbols 2949 d->symbol->save(writer); 2950 2951 // Values 2952 writer->writeStartElement(QStringLiteral("values")); 2953 writer->writeAttribute(QStringLiteral("type"), QString::number(static_cast<int>(d->valuesType))); 2954 writer->writeAttribute(QStringLiteral("valuesColumn"), d->valuesColumnPath); 2955 writer->writeAttribute(QStringLiteral("position"), QString::number(static_cast<int>(d->valuesPosition))); 2956 writer->writeAttribute(QStringLiteral("distance"), QString::number(d->valuesDistance)); 2957 writer->writeAttribute(QStringLiteral("rotation"), QString::number(d->valuesRotationAngle)); 2958 writer->writeAttribute(QStringLiteral("opacity"), QString::number(d->valuesOpacity)); 2959 writer->writeAttribute(QStringLiteral("numericFormat"), QChar::fromLatin1(d->valuesNumericFormat)); 2960 writer->writeAttribute(QStringLiteral("dateTimeFormat"), d->valuesDateTimeFormat); 2961 writer->writeAttribute(QStringLiteral("precision"), QString::number(d->valuesPrecision)); 2962 writer->writeAttribute(QStringLiteral("prefix"), d->valuesPrefix); 2963 writer->writeAttribute(QStringLiteral("suffix"), d->valuesSuffix); 2964 WRITE_QCOLOR(d->valuesColor); 2965 WRITE_QFONT(d->valuesFont); 2966 writer->writeEndElement(); 2967 2968 // Filling 2969 d->background->save(writer); 2970 2971 // Error bars 2972 writer->writeStartElement(QStringLiteral("errorBars")); 2973 d->xErrorBar->save(writer); 2974 d->yErrorBar->save(writer); 2975 d->errorBarStyle->save(writer); 2976 writer->writeEndElement(); 2977 2978 // margin plots 2979 writer->writeStartElement(QStringLiteral("margins")); 2980 writer->writeAttribute(QStringLiteral("rugEnabled"), QString::number(d->rugEnabled)); 2981 writer->writeAttribute(QStringLiteral("rugOrientation"), QString::number(static_cast<int>(d->rugOrientation))); 2982 writer->writeAttribute(QStringLiteral("rugLength"), QString::number(d->rugLength)); 2983 writer->writeAttribute(QStringLiteral("rugWidth"), QString::number(d->rugWidth)); 2984 writer->writeAttribute(QStringLiteral("rugOffset"), QString::number(d->rugOffset)); 2985 writer->writeEndElement(); 2986 2987 writer->writeEndElement(); // close "xyCurve" section 2988 } 2989 2990 //! Load from XML 2991 bool XYCurve::load(XmlStreamReader* reader, bool preview) { 2992 Q_D(XYCurve); 2993 2994 if (!readBasicAttributes(reader)) 2995 return false; 2996 2997 QXmlStreamAttributes attribs; 2998 QString str; 2999 3000 while (!reader->atEnd()) { 3001 reader->readNext(); 3002 if (reader->isEndElement() && reader->name() == QLatin1String("xyCurve")) 3003 break; 3004 3005 if (!reader->isStartElement()) 3006 continue; 3007 3008 if (reader->name() == QLatin1String("comment")) { 3009 if (!readCommentElement(reader)) 3010 return false; 3011 } else if (reader->name() == QLatin1String("general")) { 3012 attribs = reader->attributes(); 3013 READ_COLUMN(xColumn); 3014 READ_COLUMN(yColumn); 3015 READ_INT_VALUE("legendVisible", legendVisible, bool); 3016 3017 str = attribs.value(QStringLiteral("visible")).toString(); 3018 if (str.isEmpty()) 3019 reader->raiseMissingAttributeWarning(QStringLiteral("visible")); 3020 else 3021 d->setVisible(str.toInt()); 3022 READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int); 3023 } else if (!preview && reader->name() == QLatin1String("lines")) { 3024 attribs = reader->attributes(); 3025 3026 READ_INT_VALUE("type", lineType, LineType); 3027 READ_INT_VALUE("skipGaps", lineSkipGaps, bool); 3028 READ_INT_VALUE("increasingXOnly", lineIncreasingXOnly, bool); 3029 READ_INT_VALUE("interpolationPointsCount", lineInterpolationPointsCount, int); 3030 d->line->load(reader, preview); 3031 } else if (!preview && reader->name() == QLatin1String("dropLines")) { 3032 d->dropLine->load(reader, preview); 3033 } else if (!preview && reader->name() == QLatin1String("symbols")) { 3034 d->symbol->load(reader, preview); 3035 } else if (!preview && reader->name() == QLatin1String("values")) { 3036 attribs = reader->attributes(); 3037 3038 READ_INT_VALUE("type", valuesType, ValuesType); 3039 READ_COLUMN(valuesColumn); 3040 3041 READ_INT_VALUE("position", valuesPosition, ValuesPosition); 3042 READ_DOUBLE_VALUE("distance", valuesDistance); 3043 READ_DOUBLE_VALUE("rotation", valuesRotationAngle); 3044 READ_DOUBLE_VALUE("opacity", valuesOpacity); 3045 3046 str = attribs.value(QStringLiteral("numericFormat")).toString(); 3047 if (str.isEmpty()) 3048 reader->raiseMissingAttributeWarning(QStringLiteral("numericFormat")); 3049 else 3050 d->valuesNumericFormat = *(str.toLatin1().data()); 3051 3052 READ_STRING_VALUE("dateTimeFormat", valuesDateTimeFormat); 3053 READ_INT_VALUE("precision", valuesPrecision, int); 3054 3055 // don't produce any warning if no prefix or suffix is set (empty string is allowed here in xml) 3056 d->valuesPrefix = attribs.value(QStringLiteral("prefix")).toString(); 3057 d->valuesSuffix = attribs.value(QStringLiteral("suffix")).toString(); 3058 3059 READ_QCOLOR(d->valuesColor); 3060 READ_QFONT(d->valuesFont); 3061 } else if (!preview && reader->name() == QLatin1String("filling")) 3062 d->background->load(reader, preview); 3063 else if (reader->name() == QLatin1String("errorBars")) { 3064 d->xErrorBar->load(reader, preview); 3065 d->yErrorBar->load(reader, preview); 3066 d->errorBarStyle->load(reader, preview); 3067 } else if (!preview && reader->name() == QLatin1String("margins")) { 3068 attribs = reader->attributes(); 3069 3070 READ_INT_VALUE("rugEnabled", rugEnabled, bool); 3071 READ_INT_VALUE("rugOrientation", rugOrientation, Orientation); 3072 READ_DOUBLE_VALUE("rugLength", rugLength); 3073 READ_DOUBLE_VALUE("rugWidth", rugWidth); 3074 READ_DOUBLE_VALUE("rugOffset", rugOffset); 3075 } else { // unknown element 3076 reader->raiseUnknownElementWarning(); 3077 if (!reader->skipToEndElement()) 3078 return false; 3079 } 3080 } 3081 3082 return true; 3083 } 3084 3085 // ############################################################################## 3086 // ######################### Theme management ################################## 3087 // ############################################################################## 3088 void XYCurve::loadThemeConfig(const KConfig& config) { 3089 KConfigGroup group = config.group(QStringLiteral("XYCurve")); 3090 3091 const auto* plot = dynamic_cast<const CartesianPlot*>(parentAspect()); 3092 if (!plot) 3093 return; 3094 const int index = plot->curveChildIndex(this); 3095 const QColor themeColor = plot->themeColorPalette(index); 3096 3097 Q_D(XYCurve); 3098 d->suppressRecalc = true; 3099 3100 d->line->loadThemeConfig(group, themeColor); 3101 d->dropLine->loadThemeConfig(group, themeColor); 3102 d->symbol->loadThemeConfig(group, themeColor); 3103 d->background->loadThemeConfig(group); 3104 d->errorBarStyle->loadThemeConfig(group, themeColor); 3105 3106 // Values 3107 this->setValuesOpacity(group.readEntry(QStringLiteral("ValuesOpacity"), 1.0)); 3108 this->setValuesColor(group.readEntry(QStringLiteral("ValuesColor"), themeColor)); 3109 3110 // margins 3111 if (plot->theme() == QLatin1String("Tufte")) { 3112 if (d->xColumn && d->xColumn->rowCount() < 100) { 3113 setRugEnabled(true); 3114 setRugOrientation(WorksheetElement::Orientation::Both); 3115 } 3116 } else 3117 setRugEnabled(false); 3118 3119 d->suppressRecalc = false; 3120 d->recalcShapeAndBoundingRect(); 3121 } 3122 3123 void XYCurve::saveThemeConfig(const KConfig& config) { 3124 KConfigGroup group = config.group(QStringLiteral("XYCurve")); 3125 Q_D(const XYCurve); 3126 3127 d->line->saveThemeConfig(group); 3128 d->dropLine->saveThemeConfig(group); 3129 d->background->saveThemeConfig(group); 3130 d->symbol->saveThemeConfig(group); 3131 d->errorBarStyle->saveThemeConfig(group); 3132 3133 // Values 3134 group.writeEntry(QStringLiteral("ValuesOpacity"), this->valuesOpacity()); 3135 group.writeEntry(QStringLiteral("ValuesColor"), (QColor)this->valuesColor()); 3136 group.writeEntry(QStringLiteral("ValuesFont"), this->valuesFont()); 3137 3138 const int index = parentAspect()->indexOfChild<XYCurve>(this); 3139 if (index < 5) { 3140 KConfigGroup themeGroup = config.group(QStringLiteral("Theme")); 3141 for (int i = index; i < 5; i++) { 3142 QString s = QStringLiteral("ThemePaletteColor") + QString::number(i + 1); 3143 themeGroup.writeEntry(s, (QColor)d->line->pen().color()); 3144 } 3145 } 3146 }