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 }