File indexing completed on 2024-05-26 04:23:51
0001 /* 0002 * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved. 0003 * 0004 * This file is part of the KD Chart library. 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "KChartCartesianCoordinatePlane.h" 0010 #include "KChartCartesianCoordinatePlane_p.h" 0011 0012 #include "KChartAbstractDiagram.h" 0013 #include "KChartAbstractDiagram_p.h" 0014 #include "KChartAbstractCartesianDiagram.h" 0015 #include "CartesianCoordinateTransformation.h" 0016 #include "KChartGridAttributes.h" 0017 #include "KChartPaintContext.h" 0018 #include "KChartPainterSaver_p.h" 0019 #include "KChartBarDiagram.h" 0020 #include "KChartStockDiagram.h" 0021 0022 #include <QApplication> 0023 #include <QFont> 0024 #include <QList> 0025 #include <QtDebug> 0026 #include <QPainter> 0027 #include <QTime> 0028 #include <QElapsedTimer> 0029 0030 0031 using namespace KChart; 0032 0033 #define d d_func() 0034 0035 CartesianCoordinatePlane::Private::Private() 0036 : AbstractCoordinatePlane::Private() 0037 , bPaintIsRunning( false ) 0038 , hasOwnGridAttributesHorizontal( false ) 0039 , hasOwnGridAttributesVertical( false ) 0040 , isometricScaling( false ) 0041 , horizontalMin( 0 ) 0042 , horizontalMax( 0 ) 0043 , verticalMin( 0 ) 0044 , verticalMax( 0 ) 0045 , autoAdjustHorizontalRangeToData( 67 ) 0046 , autoAdjustVerticalRangeToData( 67 ) 0047 , autoAdjustGridToZoom( true ) 0048 , fixedDataCoordinateSpaceRelation( false ) 0049 , xAxisStartAtZero( true ) 0050 , reverseVerticalPlane( false ) 0051 , reverseHorizontalPlane( false ) 0052 { 0053 } 0054 0055 CartesianCoordinatePlane::CartesianCoordinatePlane( Chart* parent ) 0056 : AbstractCoordinatePlane( new Private(), parent ) 0057 { 0058 // this block left empty intentionally 0059 } 0060 0061 CartesianCoordinatePlane::~CartesianCoordinatePlane() 0062 { 0063 // this block left empty intentionally 0064 } 0065 0066 void CartesianCoordinatePlane::init() 0067 { 0068 // this block left empty intentionally 0069 } 0070 0071 0072 void CartesianCoordinatePlane::addDiagram( AbstractDiagram* diagram ) 0073 { 0074 Q_ASSERT_X( dynamic_cast<AbstractCartesianDiagram*>( diagram ), 0075 "CartesianCoordinatePlane::addDiagram", "Only cartesian " 0076 "diagrams can be added to a cartesian coordinate plane!" ); 0077 AbstractCoordinatePlane::addDiagram( diagram ); 0078 connect( diagram, &AbstractDiagram::layoutChanged, 0079 this, &CartesianCoordinatePlane::slotLayoutChanged ); 0080 0081 connect( diagram, &AbstractDiagram::propertiesChanged, this, &CartesianCoordinatePlane::propertiesChanged ); 0082 } 0083 0084 0085 void CartesianCoordinatePlane::paint( QPainter* painter ) 0086 { 0087 // prevent recursive call: 0088 if ( d->bPaintIsRunning ) { 0089 return; 0090 } 0091 d->bPaintIsRunning = true; 0092 0093 AbstractDiagramList diags = diagrams(); 0094 if ( !diags.isEmpty() ) 0095 { 0096 PaintContext ctx; 0097 ctx.setPainter ( painter ); 0098 ctx.setCoordinatePlane ( this ); 0099 const QRectF drawArea( drawingArea() ); 0100 ctx.setRectangle ( drawArea ); 0101 0102 // enabling clipping so that we're not drawing outside 0103 PainterSaver painterSaver( painter ); 0104 QRect clipRect = drawArea.toRect().adjusted( -1, -1, 1, 1 ); 0105 QRegion clipRegion( clipRect ); 0106 painter->setClipRegion( clipRegion ); 0107 0108 // paint the coordinate system rulers: 0109 d->grid->drawGrid( &ctx ); 0110 0111 // paint the diagrams: 0112 for ( int i = 0; i < diags.size(); i++ ) 0113 { 0114 if ( diags[i]->isHidden() ) { 0115 continue; 0116 } 0117 bool doDumpPaintTime = AbstractDiagram::Private::get( diags[ i ] )->doDumpPaintTime; 0118 QElapsedTimer stopWatch; 0119 if ( doDumpPaintTime ) { 0120 stopWatch.start(); 0121 } 0122 0123 PainterSaver diagramPainterSaver( painter ); 0124 diags[i]->paint( &ctx ); 0125 0126 if ( doDumpPaintTime ) { 0127 qDebug() << "Painting diagram" << i << "took" << stopWatch.elapsed() << "milliseconds"; 0128 } 0129 } 0130 0131 } 0132 d->bPaintIsRunning = false; 0133 } 0134 0135 0136 void CartesianCoordinatePlane::slotLayoutChanged( AbstractDiagram* ) 0137 { 0138 layoutDiagrams(); 0139 } 0140 0141 QRectF CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams() const 0142 { 0143 // determine unit of the rectangles of all involved diagrams: 0144 qreal minX = 0; 0145 qreal maxX = 0; 0146 qreal minY = 0; 0147 qreal maxY = 0; 0148 bool bStarting = true; 0149 const auto ds = diagrams(); 0150 for (const AbstractDiagram* diagram : ds) 0151 { 0152 QPair<QPointF, QPointF> dataBoundariesPair = diagram->dataBoundaries(); 0153 //qDebug() << "CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams()\ngets diagram->dataBoundaries: " << dataBoundariesPair.first << dataBoundariesPair.second; 0154 if ( bStarting || dataBoundariesPair.first.x() < minX ) minX = dataBoundariesPair.first.x(); 0155 if ( bStarting || dataBoundariesPair.first.y() < minY ) minY = dataBoundariesPair.first.y(); 0156 if ( bStarting || dataBoundariesPair.second.x() > maxX ) maxX = dataBoundariesPair.second.x(); 0157 if ( bStarting || dataBoundariesPair.second.y() > maxY ) maxY = dataBoundariesPair.second.y(); 0158 bStarting = false; 0159 } 0160 //qDebug() << "CartesianCoordinatePlane::getRawDataBoundingRectFromDiagrams()\nreturns data boundaries: " << QRectF( QPointF(minX, minY), QSizeF(maxX - minX, maxY - minY) ); 0161 QRectF dataBoundingRect; 0162 dataBoundingRect.setBottomLeft( QPointF( minX, minY ) ); 0163 dataBoundingRect.setTopRight( QPointF( maxX, maxY ) ); 0164 return dataBoundingRect; 0165 } 0166 0167 0168 QRectF CartesianCoordinatePlane::adjustedToMaxEmptyInnerPercentage( 0169 const QRectF& r, unsigned int percentX, unsigned int percentY ) const 0170 { 0171 QRectF ret = r; 0172 if ( ( axesCalcModeX() != Logarithmic || r.left() < 0.0 ) && percentX > 0 && percentX != 100 ) { 0173 const bool isPositive = r.left() >= 0; 0174 if ( ( r.right() >= 0 ) == isPositive ) { 0175 qreal upperBound = qMax( r.left(), r.right() ); 0176 qreal lowerBound = qMin( r.left(), r.right() ); 0177 qreal innerBound = isPositive ? lowerBound : upperBound; 0178 qreal outerBound = isPositive ? upperBound : lowerBound; 0179 if ( innerBound / outerBound * 100 <= percentX && d->xAxisStartAtZero ) { 0180 if ( isPositive ) { 0181 ret.setLeft( 0.0 ); 0182 } else { 0183 ret.setRight( 0.0 ); 0184 } 0185 } 0186 } 0187 } 0188 // ### this doesn't seem to take into account that Qt's y coordinate is inverted 0189 if ( ( axesCalcModeY() != Logarithmic || r.bottom() < 0.0 ) && percentY > 0 && percentY != 100 ) { 0190 const bool isPositive = r.bottom() >= 0; 0191 if ( ( r.top() >= 0 ) == isPositive ) { 0192 qreal upperBound = qMax( r.top(), r.bottom() ); 0193 qreal lowerBound = qMin( r.top(), r.bottom() ); 0194 const qreal innerBound = isPositive ? lowerBound : upperBound; 0195 const qreal outerBound = isPositive ? upperBound : lowerBound; 0196 if ( innerBound / outerBound * 100 <= percentY ) { 0197 if ( isPositive ) { 0198 ret.setBottom( 0.0 ); 0199 } else { 0200 ret.setTop( 0.0 ); 0201 } 0202 } 0203 } 0204 } 0205 return ret; 0206 } 0207 0208 0209 QRectF CartesianCoordinatePlane::calculateRawDataBoundingRect() const 0210 { 0211 // are manually set ranges to be applied? 0212 const bool bAutoAdjustHorizontalRange = d->autoAdjustHorizontalRangeToData < 100; 0213 const bool bAutoAdjustVerticalRange = d->autoAdjustVerticalRangeToData < 100; 0214 0215 const bool bHardHorizontalRange = (!bAutoAdjustHorizontalRange) && (d->horizontalMin != d->horizontalMax || (ISNAN(d->horizontalMin) != ISNAN(d->horizontalMax))); 0216 const bool bHardVerticalRange = (!bAutoAdjustVerticalRange) && (d->verticalMin != d->verticalMax || (ISNAN(d->verticalMin) != ISNAN(d->verticalMax))); 0217 QRectF dataBoundingRect; 0218 0219 // if custom boundaries are set on the plane, use them 0220 if ( bHardHorizontalRange && bHardVerticalRange ) { 0221 dataBoundingRect.setLeft( d->horizontalMin ); 0222 dataBoundingRect.setRight( d->horizontalMax ); 0223 dataBoundingRect.setBottom( d->verticalMin ); 0224 dataBoundingRect.setTop( d->verticalMax ); 0225 } else { 0226 // determine unit of the rectangles of all involved diagrams: 0227 dataBoundingRect = getRawDataBoundingRectFromDiagrams(); 0228 if ( bHardHorizontalRange ) { 0229 if (!ISNAN(d->horizontalMin)) 0230 dataBoundingRect.setLeft( d->horizontalMin ); 0231 if (!ISNAN(d->horizontalMax)) 0232 dataBoundingRect.setRight( d->horizontalMax ); 0233 } 0234 if ( bHardVerticalRange ) { 0235 if (!ISNAN(d->verticalMin)) 0236 dataBoundingRect.setBottom( d->verticalMin ); 0237 if (!ISNAN(d->verticalMax)) 0238 dataBoundingRect.setTop( d->verticalMax ); 0239 } 0240 } 0241 // recalculate the bounds, if automatic adjusting of ranges is desired AND 0242 // both bounds are at the same side of the zero line 0243 dataBoundingRect = adjustedToMaxEmptyInnerPercentage( 0244 dataBoundingRect, d->autoAdjustHorizontalRangeToData, d->autoAdjustVerticalRangeToData ); 0245 if ( bAutoAdjustHorizontalRange ) { 0246 const_cast<CartesianCoordinatePlane*>( this )->d->horizontalMin = dataBoundingRect.left(); 0247 const_cast<CartesianCoordinatePlane*>( this )->d->horizontalMax = dataBoundingRect.right(); 0248 } 0249 if ( bAutoAdjustVerticalRange ) { 0250 const_cast<CartesianCoordinatePlane*>( this )->d->verticalMin = dataBoundingRect.bottom(); 0251 const_cast<CartesianCoordinatePlane*>( this )->d->verticalMax = dataBoundingRect.top(); 0252 } 0253 // qDebug() << Q_FUNC_INFO << dataBoundingRect; 0254 return dataBoundingRect; 0255 } 0256 0257 0258 DataDimensionsList CartesianCoordinatePlane::getDataDimensionsList() const 0259 { 0260 const AbstractCartesianDiagram* dgr = diagrams().isEmpty() ? nullptr : 0261 qobject_cast< const AbstractCartesianDiagram* >( diagrams().first() ); 0262 if ( dgr && dgr->referenceDiagram() ) { 0263 dgr = dgr->referenceDiagram(); 0264 } 0265 const BarDiagram *barDiagram = qobject_cast< const BarDiagram* >( dgr ); 0266 const StockDiagram *stockDiagram = qobject_cast< const StockDiagram* >( dgr ); 0267 0268 // note: 0269 // It does make sense to retrieve the orientation from the first diagram. This is because 0270 // a coordinate plane can either be for horizontal *or* for vertical diagrams. Both at the 0271 // same time won't work, and thus the orientation for all diagrams is the same as for the first one. 0272 const Qt::Orientation diagramOrientation = barDiagram != nullptr ? barDiagram->orientation() : Qt::Vertical; 0273 const bool diagramIsVertical = diagramOrientation == Qt::Vertical; 0274 0275 DataDimensionsList l; 0276 if ( dgr ) { 0277 const QRectF r( calculateRawDataBoundingRect() ); 0278 // We do not access d->gridAttributesHorizontal/Vertical here, but we use the getter function, 0279 // to get the global attrs, if no special ones have been set for the given orientation. 0280 const GridAttributes gaH( gridAttributes( Qt::Horizontal ) ); 0281 const GridAttributes gaV( gridAttributes( Qt::Vertical ) ); 0282 // append the first dimension: for Abscissa axes 0283 l.append( 0284 DataDimension( 0285 r.left(), r.right(), 0286 diagramIsVertical ? ( !stockDiagram && dgr->datasetDimension() > 1 ) : true, 0287 axesCalcModeX(), 0288 gaH.gridGranularitySequence(), 0289 gaH.gridStepWidth(), 0290 gaH.gridSubStepWidth() ) ); 0291 // append the second dimension: for Ordinate axes 0292 l.append( 0293 DataDimension( 0294 r.bottom(), r.top(), 0295 diagramIsVertical ? true : ( dgr->datasetDimension() > 1 ), 0296 axesCalcModeY(), 0297 gaV.gridGranularitySequence(), 0298 gaV.gridStepWidth(), 0299 gaV.gridSubStepWidth() ) ); 0300 } else { 0301 l.append( DataDimension() ); // This gets us the default 1..0 / 1..0 grid 0302 l.append( DataDimension() ); // shown, if there is no diagram on this plane. 0303 } 0304 return l; 0305 } 0306 0307 QRectF CartesianCoordinatePlane::drawingArea() const 0308 { 0309 // the rectangle the diagrams cover in the *plane*: 0310 // We reserve 1px on each side for antialiased drawing, and respect the way QPainter calculates 0311 // the width of a painted rect (the size is the rectangle size plus the pen width). The latter 0312 // accounts for another pixel that we subtract from height and width. 0313 // This way, most clipping for regular pens should be avoided. When pens with a width larger 0314 // than 1 are used, this may not be sufficient. 0315 return QRectF( areaGeometry() ).adjusted( 1.0, 1.0, -2.0, -2.0 ); 0316 } 0317 0318 0319 QRectF CartesianCoordinatePlane::logicalArea() const 0320 { 0321 if ( d->dimensions.isEmpty() ) 0322 return QRectF(); 0323 0324 const DataDimension dimX = d->dimensions.first(); 0325 const DataDimension dimY = d->dimensions.last(); 0326 const QPointF pt( qMin( dimX.start, dimX.end ), qMax( dimY.start, dimY.end ) ); 0327 const QSizeF siz( qAbs( dimX.distance() ), -qAbs( dimY.distance() ) ); 0328 const QRectF dataBoundingRect( pt, siz ); 0329 0330 // determine logical top left, taking the "reverse" options into account 0331 const QPointF topLeft( d->reverseHorizontalPlane ? dataBoundingRect.right() : dataBoundingRect.left(), 0332 d->reverseVerticalPlane ? dataBoundingRect.bottom() : dataBoundingRect.top() ); 0333 0334 const qreal width = dataBoundingRect.width() * ( d->reverseHorizontalPlane ? -1.0 : 1.0 ); 0335 const qreal height = dataBoundingRect.height() * ( d->reverseVerticalPlane ? -1.0 : 1.0 ); 0336 0337 return QRectF( topLeft, QSizeF( width, height ) ); 0338 } 0339 0340 QRectF CartesianCoordinatePlane::diagramArea() const 0341 { 0342 const QRectF logArea( logicalArea() ); 0343 QPointF physicalTopLeft = d->coordinateTransformation.translate( logArea.topLeft() ); 0344 QPointF physicalBottomRight = d->coordinateTransformation.translate( logArea.bottomRight() ); 0345 0346 return QRectF( physicalTopLeft, physicalBottomRight ).normalized(); 0347 } 0348 0349 QRectF CartesianCoordinatePlane::visibleDiagramArea() const 0350 { 0351 return diagramArea().intersected( drawingArea() ); 0352 } 0353 0354 void CartesianCoordinatePlane::layoutDiagrams() 0355 { 0356 d->dimensions = gridDimensionsList(); 0357 Q_ASSERT_X ( d->dimensions.count() == 2, "CartesianCoordinatePlane::layoutDiagrams", 0358 "Error: gridDimensionsList() did not return exactly two dimensions." ); 0359 0360 // physical area of the plane 0361 const QRectF physicalArea( drawingArea() ); 0362 // .. in contrast to the logical area 0363 const QRectF logArea( logicalArea() ); 0364 0365 // TODO: isometric scaling for zooming? 0366 0367 // the plane area might have changed, so the zoom values might also be changed 0368 handleFixedDataCoordinateSpaceRelation( physicalArea ); 0369 0370 d->coordinateTransformation.updateTransform( logArea, physicalArea ); 0371 0372 update(); 0373 } 0374 0375 void CartesianCoordinatePlane::setFixedDataCoordinateSpaceRelation( bool fixed ) 0376 { 0377 d->fixedDataCoordinateSpaceRelation = fixed; 0378 d->fixedDataCoordinateSpaceRelationPinnedSize = QSize(); 0379 handleFixedDataCoordinateSpaceRelation( drawingArea() ); 0380 } 0381 0382 bool CartesianCoordinatePlane::hasFixedDataCoordinateSpaceRelation() const 0383 { 0384 return d->fixedDataCoordinateSpaceRelation; 0385 } 0386 0387 void CartesianCoordinatePlane::setXAxisStartAtZero(bool fixedStart) 0388 { 0389 if (d->xAxisStartAtZero == fixedStart) 0390 return; 0391 0392 d->xAxisStartAtZero = fixedStart; 0393 } 0394 0395 bool CartesianCoordinatePlane::xAxisStartAtZero() const 0396 { 0397 return d->xAxisStartAtZero; 0398 } 0399 0400 void CartesianCoordinatePlane::handleFixedDataCoordinateSpaceRelation( const QRectF& geometry ) 0401 { 0402 if ( !d->fixedDataCoordinateSpaceRelation ) { 0403 return; 0404 } 0405 // is the new geometry ok? 0406 if ( !geometry.isValid() ) { 0407 return; 0408 } 0409 0410 // note that the pinned size can be invalid even after setting it, if geometry wasn't valid. 0411 // this is relevant for the cooperation between this method, setFixedDataCoordinateSpaceRelation(), 0412 // and handleFixedDataCoordinateSpaceRelation(). 0413 if ( !d->fixedDataCoordinateSpaceRelationPinnedSize.isValid() ) { 0414 d->fixedDataCoordinateSpaceRelationPinnedSize = geometry.size(); 0415 d->fixedDataCoordinateSpaceRelationPinnedZoom = ZoomParameters( zoomFactorX(), zoomFactorY(), zoomCenter() ); 0416 return; 0417 } 0418 0419 // if the plane size was changed, change zoom factors to keep the diagram size constant 0420 if ( d->fixedDataCoordinateSpaceRelationPinnedSize != geometry.size() ) { 0421 const qreal widthScaling = d->fixedDataCoordinateSpaceRelationPinnedSize.width() / geometry.width(); 0422 const qreal heightScaling = d->fixedDataCoordinateSpaceRelationPinnedSize.height() / geometry.height(); 0423 0424 const qreal newZoomX = d->fixedDataCoordinateSpaceRelationPinnedZoom.xFactor * widthScaling; 0425 const qreal newZoomY = d->fixedDataCoordinateSpaceRelationPinnedZoom.yFactor * heightScaling; 0426 0427 const QPointF newCenter = QPointF( d->fixedDataCoordinateSpaceRelationPinnedZoom.xCenter / widthScaling, 0428 d->fixedDataCoordinateSpaceRelationPinnedZoom.yCenter / heightScaling ); 0429 // Use these internal methods to avoid sending the propertiesChanged signal more than once 0430 bool changed = false; 0431 if ( doneSetZoomFactorY( newZoomY ) ) 0432 changed = true; 0433 if ( doneSetZoomFactorX( newZoomX ) ) 0434 changed = true; 0435 if ( doneSetZoomCenter( newCenter ) ) 0436 changed = true; 0437 if ( changed ) 0438 Q_EMIT propertiesChanged(); 0439 } 0440 } 0441 0442 const QPointF CartesianCoordinatePlane::translate( const QPointF& diagramPoint ) const 0443 { 0444 // Note: We do not test if the point lays inside of the data area, 0445 // but we just apply the transformation calculations to the point. 0446 // This allows for basic calculations done by the user, see e.g. 0447 // the file examples/Lines/BubbleChart/mainwindow.cpp 0448 return d->coordinateTransformation.translate( diagramPoint ); 0449 } 0450 0451 const QPointF CartesianCoordinatePlane::translateBack( const QPointF& screenPoint ) const 0452 { 0453 return d->coordinateTransformation.translateBack( screenPoint ); 0454 } 0455 0456 void CartesianCoordinatePlane::setIsometricScaling( bool isOn ) 0457 { 0458 if ( d->isometricScaling != isOn ) { 0459 d->isometricScaling = isOn; 0460 layoutDiagrams(); 0461 Q_EMIT propertiesChanged(); 0462 } 0463 } 0464 0465 bool CartesianCoordinatePlane::doesIsometricScaling() const 0466 { 0467 return d->isometricScaling; 0468 } 0469 0470 bool CartesianCoordinatePlane::doneSetZoomFactorX( qreal factor ) 0471 { 0472 if ( d->coordinateTransformation.zoom.xFactor == factor ) { 0473 return false; 0474 } 0475 d->coordinateTransformation.zoom.xFactor = factor; 0476 if ( d->autoAdjustGridToZoom ) { 0477 d->grid->setNeedRecalculate(); 0478 } 0479 return true; 0480 } 0481 0482 bool CartesianCoordinatePlane::doneSetZoomFactorY( qreal factor ) 0483 { 0484 if ( d->coordinateTransformation.zoom.yFactor == factor ) { 0485 return false; 0486 } 0487 d->coordinateTransformation.zoom.yFactor = factor; 0488 if ( d->autoAdjustGridToZoom ) { 0489 d->grid->setNeedRecalculate(); 0490 } 0491 return true; 0492 } 0493 0494 bool CartesianCoordinatePlane::doneSetZoomCenter( const QPointF& point ) 0495 { 0496 if ( d->coordinateTransformation.zoom.center() == point ) { 0497 return false; 0498 } 0499 d->coordinateTransformation.zoom.setCenter( point ); 0500 if ( d->autoAdjustGridToZoom ) { 0501 d->grid->setNeedRecalculate(); 0502 } 0503 return true; 0504 } 0505 0506 void CartesianCoordinatePlane::setZoomFactors( qreal factorX, qreal factorY ) 0507 { 0508 if ( doneSetZoomFactorX( factorX ) || doneSetZoomFactorY( factorY ) ) { 0509 d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); 0510 Q_EMIT propertiesChanged(); 0511 } 0512 } 0513 0514 void CartesianCoordinatePlane::setZoomFactorX( qreal factor ) 0515 { 0516 if ( doneSetZoomFactorX( factor ) ) { 0517 d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); 0518 Q_EMIT propertiesChanged(); 0519 } 0520 } 0521 0522 void CartesianCoordinatePlane::setZoomFactorY( qreal factor ) 0523 { 0524 if ( doneSetZoomFactorY( factor ) ) { 0525 d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); 0526 Q_EMIT propertiesChanged(); 0527 } 0528 } 0529 0530 void CartesianCoordinatePlane::setZoomCenter( const QPointF& point ) 0531 { 0532 if ( doneSetZoomCenter( point ) ) { 0533 d->coordinateTransformation.updateTransform( logicalArea(), drawingArea() ); 0534 Q_EMIT propertiesChanged(); 0535 } 0536 } 0537 0538 QPointF CartesianCoordinatePlane::zoomCenter() const 0539 { 0540 return d->coordinateTransformation.zoom.center(); 0541 } 0542 0543 qreal CartesianCoordinatePlane::zoomFactorX() const 0544 { 0545 return d->coordinateTransformation.zoom.xFactor; 0546 } 0547 0548 qreal CartesianCoordinatePlane::zoomFactorY() const 0549 { 0550 return d->coordinateTransformation.zoom.yFactor; 0551 } 0552 0553 0554 CartesianCoordinatePlane::AxesCalcMode CartesianCoordinatePlane::axesCalcModeY() const 0555 { 0556 return d->coordinateTransformation.axesCalcModeY; 0557 } 0558 0559 CartesianCoordinatePlane::AxesCalcMode CartesianCoordinatePlane::axesCalcModeX() const 0560 { 0561 return d->coordinateTransformation.axesCalcModeX; 0562 } 0563 0564 void CartesianCoordinatePlane::setAxesCalcModes( AxesCalcMode mode ) 0565 { 0566 if ( d->coordinateTransformation.axesCalcModeY != mode || 0567 d->coordinateTransformation.axesCalcModeX != mode ) { 0568 d->coordinateTransformation.axesCalcModeY = mode; 0569 d->coordinateTransformation.axesCalcModeX = mode; 0570 Q_EMIT propertiesChanged(); 0571 Q_EMIT viewportCoordinateSystemChanged(); 0572 const auto ds = diagrams(); 0573 for (AbstractDiagram* diag : ds) 0574 slotLayoutChanged( diag ); 0575 } 0576 } 0577 0578 void CartesianCoordinatePlane::setAxesCalcModeY( AxesCalcMode mode ) 0579 { 0580 if ( d->coordinateTransformation.axesCalcModeY != mode ) { 0581 d->coordinateTransformation.axesCalcModeY = mode; 0582 Q_EMIT propertiesChanged(); 0583 setGridNeedsRecalculate(); 0584 Q_EMIT viewportCoordinateSystemChanged(); 0585 } 0586 } 0587 0588 void CartesianCoordinatePlane::setAxesCalcModeX( AxesCalcMode mode ) 0589 { 0590 if ( d->coordinateTransformation.axesCalcModeX != mode ) { 0591 d->coordinateTransformation.axesCalcModeX = mode; 0592 Q_EMIT propertiesChanged(); 0593 Q_EMIT viewportCoordinateSystemChanged(); 0594 } 0595 } 0596 0597 namespace { 0598 inline bool fuzzyCompare( qreal a, qreal b ) 0599 { 0600 if ( ISNAN(a) && ISNAN(b) ) 0601 return true; 0602 if ( qFuzzyIsNull(a) && qFuzzyIsNull(b) ) 0603 return true; 0604 return qFuzzyCompare( a, b ); 0605 } 0606 } 0607 0608 void CartesianCoordinatePlane::setHorizontalRange( const QPair< qreal, qreal > & range ) 0609 { 0610 if ( !fuzzyCompare(d->horizontalMin, range.first) || !fuzzyCompare(d->horizontalMax, range.second) ) { 0611 d->autoAdjustHorizontalRangeToData = 100; 0612 d->horizontalMin = range.first; 0613 d->horizontalMax = range.second; 0614 layoutDiagrams(); 0615 Q_EMIT propertiesChanged(); 0616 Q_EMIT boundariesChanged(); 0617 } 0618 } 0619 0620 void CartesianCoordinatePlane::setVerticalRange( const QPair< qreal, qreal > & range ) 0621 { 0622 if ( !fuzzyCompare(d->verticalMin, range.first) || !fuzzyCompare(d->verticalMax, range.second) ) { 0623 d->autoAdjustVerticalRangeToData = 100; 0624 d->verticalMin = range.first; 0625 d->verticalMax = range.second; 0626 layoutDiagrams(); 0627 Q_EMIT propertiesChanged(); 0628 Q_EMIT boundariesChanged(); 0629 } 0630 } 0631 0632 QPair< qreal, qreal > CartesianCoordinatePlane::horizontalRange() const 0633 { 0634 return QPair<qreal, qreal>( d->horizontalMin, d->horizontalMax ); 0635 } 0636 0637 QPair< qreal, qreal > CartesianCoordinatePlane::verticalRange() const 0638 { 0639 return QPair<qreal, qreal>( d->verticalMin, d->verticalMax ); 0640 } 0641 0642 void CartesianCoordinatePlane::adjustRangesToData() 0643 { 0644 const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); 0645 d->horizontalMin = dataBoundingRect.left(); 0646 d->horizontalMax = dataBoundingRect.right(); 0647 d->verticalMin = dataBoundingRect.top(); 0648 d->verticalMax = dataBoundingRect.bottom(); 0649 layoutDiagrams(); 0650 Q_EMIT propertiesChanged(); 0651 } 0652 0653 void CartesianCoordinatePlane::adjustHorizontalRangeToData() 0654 { 0655 const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); 0656 d->horizontalMin = dataBoundingRect.left(); 0657 d->horizontalMax = dataBoundingRect.right(); 0658 layoutDiagrams(); 0659 Q_EMIT propertiesChanged(); 0660 } 0661 0662 void CartesianCoordinatePlane::adjustVerticalRangeToData() 0663 { 0664 const QRectF dataBoundingRect( getRawDataBoundingRectFromDiagrams() ); 0665 d->verticalMin = dataBoundingRect.bottom(); 0666 d->verticalMax = dataBoundingRect.top(); 0667 layoutDiagrams(); 0668 Q_EMIT propertiesChanged(); 0669 } 0670 0671 void CartesianCoordinatePlane::setAutoAdjustHorizontalRangeToData( unsigned int percentEmpty ) 0672 { 0673 if ( d->autoAdjustHorizontalRangeToData != percentEmpty ) 0674 { 0675 d->autoAdjustHorizontalRangeToData = percentEmpty; 0676 d->horizontalMin = 0.0; 0677 d->horizontalMax = 0.0; 0678 layoutDiagrams(); 0679 Q_EMIT propertiesChanged(); 0680 } 0681 } 0682 0683 void CartesianCoordinatePlane::setAutoAdjustVerticalRangeToData( unsigned int percentEmpty ) 0684 { 0685 if ( d->autoAdjustVerticalRangeToData != percentEmpty ) 0686 { 0687 d->autoAdjustVerticalRangeToData = percentEmpty; 0688 d->verticalMin = 0.0; 0689 d->verticalMax = 0.0; 0690 layoutDiagrams(); 0691 Q_EMIT propertiesChanged(); 0692 } 0693 } 0694 0695 unsigned int CartesianCoordinatePlane::autoAdjustHorizontalRangeToData() const 0696 { 0697 return d->autoAdjustHorizontalRangeToData; 0698 } 0699 0700 unsigned int CartesianCoordinatePlane::autoAdjustVerticalRangeToData() const 0701 { 0702 return d->autoAdjustVerticalRangeToData; 0703 } 0704 0705 void CartesianCoordinatePlane::setGridAttributes( 0706 Qt::Orientation orientation, 0707 const GridAttributes& a ) 0708 { 0709 if ( orientation == Qt::Horizontal ) 0710 d->gridAttributesHorizontal = a; 0711 else 0712 d->gridAttributesVertical = a; 0713 setHasOwnGridAttributes( orientation, true ); 0714 update(); 0715 Q_EMIT propertiesChanged(); 0716 } 0717 0718 void CartesianCoordinatePlane::resetGridAttributes( Qt::Orientation orientation ) 0719 { 0720 setHasOwnGridAttributes( orientation, false ); 0721 update(); 0722 } 0723 0724 const GridAttributes CartesianCoordinatePlane::gridAttributes( Qt::Orientation orientation ) const 0725 { 0726 if ( hasOwnGridAttributes( orientation ) ) { 0727 if ( orientation == Qt::Horizontal ) 0728 return d->gridAttributesHorizontal; 0729 else 0730 return d->gridAttributesVertical; 0731 } else { 0732 return globalGridAttributes(); 0733 } 0734 } 0735 0736 void CartesianCoordinatePlane::setHasOwnGridAttributes( Qt::Orientation orientation, bool on ) 0737 { 0738 if ( orientation == Qt::Horizontal ) 0739 d->hasOwnGridAttributesHorizontal = on; 0740 else 0741 d->hasOwnGridAttributesVertical = on; 0742 Q_EMIT propertiesChanged(); 0743 } 0744 0745 bool CartesianCoordinatePlane::hasOwnGridAttributes( Qt::Orientation orientation ) const 0746 { 0747 return orientation == Qt::Horizontal ? d->hasOwnGridAttributesHorizontal 0748 : d->hasOwnGridAttributesVertical; 0749 } 0750 0751 void CartesianCoordinatePlane::setAutoAdjustGridToZoom( bool autoAdjust ) 0752 { 0753 if ( d->autoAdjustGridToZoom != autoAdjust ) { 0754 d->autoAdjustGridToZoom = autoAdjust; 0755 d->grid->setNeedRecalculate(); 0756 Q_EMIT propertiesChanged(); 0757 } 0758 } 0759 0760 #if defined(Q_COMPILER_MANGLES_RETURN_TYPE) 0761 const 0762 #endif 0763 bool CartesianCoordinatePlane::autoAdjustGridToZoom() const 0764 { 0765 return d->autoAdjustGridToZoom; 0766 } 0767 0768 AbstractCoordinatePlane* CartesianCoordinatePlane::sharedAxisMasterPlane( QPainter* painter ) 0769 { 0770 CartesianCoordinatePlane* plane = this; 0771 AbstractCartesianDiagram* diag = dynamic_cast< AbstractCartesianDiagram* >( plane->diagram() ); 0772 const CartesianAxis* sharedAxis = nullptr; 0773 if ( diag != nullptr ) 0774 { 0775 const CartesianAxisList axes = diag->axes(); 0776 for ( const CartesianAxis* a : axes ) 0777 { 0778 CartesianCoordinatePlane* p = const_cast< CartesianCoordinatePlane* >( 0779 dynamic_cast< const CartesianCoordinatePlane* >( a->coordinatePlane() ) ); 0780 if ( p != nullptr && p != this ) 0781 { 0782 plane = p; 0783 sharedAxis = a; 0784 } 0785 } 0786 } 0787 0788 if ( plane == this || painter == nullptr ) 0789 return plane; 0790 0791 const QPointF zero = QPointF( 0, 0 ); 0792 const QPointF tenX = QPointF( 10, 0 ); 0793 const QPointF tenY = QPointF( 0, 10 ); 0794 0795 0796 if ( sharedAxis->isOrdinate() ) 0797 { 0798 painter->translate( translate( zero ).x(), 0.0 ); 0799 const qreal factor = (translate( tenX ) - translate( zero ) ).x() / ( plane->translate( tenX ) - plane->translate( zero ) ).x(); 0800 painter->scale( factor, 1.0 ); 0801 painter->translate( -plane->translate( zero ).x(), 0.0 ); 0802 } 0803 if ( sharedAxis->isAbscissa() ) 0804 { 0805 painter->translate( 0.0, translate( zero ).y() ); 0806 const qreal factor = (translate( tenY ) - translate( zero ) ).y() / ( plane->translate( tenY ) - plane->translate( zero ) ).y(); 0807 painter->scale( 1.0, factor ); 0808 painter->translate( 0.0, -plane->translate( zero ).y() ); 0809 } 0810 0811 0812 return plane; 0813 } 0814 0815 void CartesianCoordinatePlane::setHorizontalRangeReversed( bool reverse ) 0816 { 0817 if ( d->reverseHorizontalPlane == reverse ) 0818 return; 0819 0820 d->reverseHorizontalPlane = reverse; 0821 layoutDiagrams(); 0822 Q_EMIT propertiesChanged(); 0823 } 0824 0825 bool CartesianCoordinatePlane::isHorizontalRangeReversed() const 0826 { 0827 return d->reverseHorizontalPlane; 0828 } 0829 0830 void CartesianCoordinatePlane::setVerticalRangeReversed( bool reverse ) 0831 { 0832 if ( d->reverseVerticalPlane == reverse ) 0833 return; 0834 0835 d->reverseVerticalPlane = reverse; 0836 layoutDiagrams(); 0837 Q_EMIT propertiesChanged(); 0838 } 0839 0840 bool CartesianCoordinatePlane::isVerticalRangeReversed() const 0841 { 0842 return d->reverseVerticalPlane; 0843 } 0844 0845 QRectF CartesianCoordinatePlane::visibleDataRange() const 0846 { 0847 QRectF result; 0848 0849 const QRectF drawArea = drawingArea(); 0850 0851 result.setTopLeft( translateBack( drawArea.topLeft() ) ); 0852 result.setBottomRight( translateBack( drawArea.bottomRight() ) ); 0853 0854 return result; 0855 } 0856 0857 void CartesianCoordinatePlane::setGeometry( const QRect& rectangle ) 0858 { 0859 if ( rectangle == geometry() ) { 0860 return; 0861 } 0862 0863 d->geometry = rectangle; 0864 if ( d->isometricScaling ) { 0865 const int hfw = heightForWidth( rectangle.width() ); 0866 // same scaling for x and y means a fixed aspect ratio, which is enforced here 0867 // always shrink the too large dimension 0868 if ( hfw < rectangle.height() ) { 0869 d->geometry.setHeight( hfw ); 0870 } else { 0871 d->geometry.setWidth( qRound( qreal( rectangle.width() ) * 0872 qreal( rectangle.height() ) / qreal( hfw ) ) ); 0873 } 0874 } 0875 0876 AbstractCoordinatePlane::setGeometry( d->geometry ); 0877 0878 const auto ds = diagrams(); 0879 for (AbstractDiagram* diagram : ds) { 0880 diagram->resize( d->geometry.size() ); 0881 } 0882 } 0883 0884 Qt::Orientations CartesianCoordinatePlane::expandingDirections() const 0885 { 0886 // not completely sure why this is required for isometric scaling... 0887 return d->isometricScaling ? Qt::Horizontal : ( Qt::Horizontal | Qt::Vertical ); 0888 } 0889 0890 bool CartesianCoordinatePlane::hasHeightForWidth() const 0891 { 0892 return d->isometricScaling; 0893 } 0894 0895 int CartesianCoordinatePlane::heightForWidth( int w ) const 0896 { 0897 // ### using anything for dataRect that depends on geometry will close a feedback loop which 0898 // prevents the geometry from stabilizing. specifically, visibleDataRange() depends on 0899 // drawingArea(), and no good will come out of using it here. 0900 QRectF dataRect = logicalArea(); 0901 return qRound( qreal( w ) * qAbs( qreal( dataRect.height() ) / qreal( dataRect.width() ) ) ); 0902 } 0903 0904 QSize CartesianCoordinatePlane::sizeHint() const 0905 { 0906 QSize sh = AbstractCoordinatePlane::sizeHint(); 0907 if ( d->isometricScaling ) { 0908 // not sure why the next line doesn't cause an infinite loop, but it improves initial size allocation 0909 sh = d->geometry.size(); 0910 sh.setHeight( heightForWidth( sh.width() ) ); 0911 } 0912 return sh; 0913 }