File indexing completed on 2024-12-15 04:02:31

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 "KChartCartesianGrid.h"
0010 #include "KChartAbstractCartesianDiagram.h"
0011 #include "KChartPaintContext.h"
0012 #include "KChartPainterSaver_p.h"
0013 #include "KChartPrintingParameters.h"
0014 #include "KChartFrameAttributes.h"
0015 #include "KChartCartesianAxis_p.h"
0016 #include "KChartMath_p.h"
0017 
0018 #include <QPainter>
0019 #include <QPainterPath>
0020 
0021 using namespace KChart;
0022 
0023 CartesianGrid::CartesianGrid()
0024     : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 )
0025 {
0026 }
0027 
0028 CartesianGrid::~CartesianGrid()
0029 {
0030 }
0031         
0032 int CartesianGrid::minimalSteps() const
0033 {
0034     return m_minsteps;
0035 }
0036 
0037 void CartesianGrid::setMinimalSteps(int minsteps)
0038 {
0039     m_minsteps = minsteps;
0040 }
0041 
0042 int CartesianGrid::maximalSteps() const
0043 {
0044     return m_maxsteps;
0045 }
0046 
0047 void CartesianGrid::setMaximalSteps(int maxsteps)
0048 {
0049     m_maxsteps = maxsteps;
0050 }
0051 
0052 void CartesianGrid::drawGrid( PaintContext* context )
0053 {
0054     CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( context->coordinatePlane() );
0055     const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
0056     const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
0057     if ( !gridAttrsX.isGridVisible() && !gridAttrsX.isSubGridVisible() &&
0058          !gridAttrsY.isGridVisible() && !gridAttrsY.isSubGridVisible() ) {
0059         return;
0060     }
0061     // This plane is used for translating the coordinates - not for the data boundaries
0062     QPainter *p = context->painter();
0063     PainterSaver painterSaver( p );
0064     // sharedAxisMasterPlane() changes the painter's coordinate transformation(!)
0065     plane = qobject_cast< CartesianCoordinatePlane* >( plane->sharedAxisMasterPlane( context->painter() ) );
0066     Q_ASSERT_X ( plane, "CartesianGrid::drawGrid",
0067                  "Bad function call: PaintContext::coodinatePlane() NOT a cartesian plane." );
0068 
0069     // update the calculated mDataDimensions before using them
0070     updateData( context->coordinatePlane() ); // this, in turn, calls our calculateGrid().
0071     Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid",
0072                  "Error: updateData did not return exactly two dimensions." );
0073     if ( !isBoundariesValid( mDataDimensions ) ) {
0074         return;
0075     }
0076 
0077     const DataDimension dimX = mDataDimensions.first();
0078     const DataDimension dimY = mDataDimensions.last();
0079     const bool isLogarithmicX = dimX.calcMode == AbstractCoordinatePlane::Logarithmic;
0080     const bool isLogarithmicY = dimY.calcMode == AbstractCoordinatePlane::Logarithmic;
0081 
0082     qreal minValueX = qMin( dimX.start, dimX.end );
0083     qreal maxValueX = qMax( dimX.start, dimX.end );
0084     qreal minValueY = qMin( dimY.start, dimY.end );
0085     qreal maxValueY = qMax( dimY.start, dimY.end );
0086     {
0087         bool adjustXLower = !isLogarithmicX && gridAttrsX.adjustLowerBoundToGrid();
0088         bool adjustXUpper = !isLogarithmicX && gridAttrsX.adjustUpperBoundToGrid();
0089         bool adjustYLower = !isLogarithmicY && gridAttrsY.adjustLowerBoundToGrid();
0090         bool adjustYUpper = !isLogarithmicY && gridAttrsY.adjustUpperBoundToGrid();
0091         AbstractGrid::adjustLowerUpperRange( minValueX, maxValueX, dimX.stepWidth, adjustXLower, adjustXUpper );
0092         AbstractGrid::adjustLowerUpperRange( minValueY, maxValueY, dimY.stepWidth, adjustYLower, adjustYUpper );
0093     }
0094 
0095    if ( plane->frameAttributes().isVisible() ) {
0096         const qreal radius = plane->frameAttributes().cornerRadius();
0097         QPainterPath path;
0098         path.addRoundedRect( QRectF( plane->translate( QPointF( minValueX, minValueY ) ),
0099                                      plane->translate( QPointF( maxValueX, maxValueY ) ) ),
0100                              radius, radius );
0101         context->painter()->setClipPath( path );
0102     }
0103 
0104     /* TODO features from old code:
0105        - MAYBE coarsen the main grid when it gets crowded (do it in calculateGrid or here?)
0106         if ( ! dimX.isCalculated ) {
0107             while ( screenRangeX / numberOfUnitLinesX <= MinimumPixelsBetweenLines ) {
0108                 dimX.stepWidth *= 10.0;
0109                 dimX.subStepWidth *= 10.0;
0110                 numberOfUnitLinesX = qAbs( dimX.distance() / dimX.stepWidth );
0111             }
0112         }
0113        - MAYBE deactivate the sub-grid when it gets crowded
0114         if ( dimX.subStepWidth && (screenRangeX / (dimX.distance() / dimX.subStepWidth)
0115              <= MinimumPixelsBetweenLines) ) {
0116             // de-activating grid sub steps: not enough space
0117             dimX.subStepWidth = 0.0;
0118         }
0119     */
0120 
0121     for ( int i = 0; i < 2; i++ ) {
0122         XySwitch xy( i == 1 ); // first iteration paints the X grid lines, second paints the Y grid lines
0123         const GridAttributes& gridAttrs = xy( gridAttrsX, gridAttrsY );
0124         bool hasMajorLines = gridAttrs.isGridVisible();
0125         bool hasMinorLines = hasMajorLines && gridAttrs.isSubGridVisible();
0126         if ( !hasMajorLines && !hasMinorLines ) {
0127             continue;
0128         }
0129 
0130         const DataDimension& dimension = xy( dimX, dimY );
0131         const bool drawZeroLine = dimension.isCalculated && gridAttrs.zeroLinePen().style() != Qt::NoPen;
0132 
0133         QPointF lineStart = QPointF( minValueX, minValueY ); // still need transformation to screen space
0134         QPointF lineEnd = QPointF( maxValueX, maxValueY );
0135 
0136         TickIterator it( xy.isY, dimension, gridAttrs.linesOnAnnotations(),
0137                          hasMajorLines, hasMinorLines, plane );
0138         for ( ; !it.isAtEnd(); ++it ) {
0139             if ( !gridAttrs.isOuterLinesVisible() &&
0140                  ( it.areAlmostEqual( it.position(), xy( minValueX, minValueY ) ) ||
0141                    it.areAlmostEqual( it.position(), xy( maxValueX, maxValueY ) ) ) ) {
0142                 continue;
0143             }
0144             xy.lvalue( lineStart.rx(), lineStart.ry() ) = it.position();
0145             xy.lvalue( lineEnd.rx(), lineEnd.ry() ) = it.position();
0146             QPointF transLineStart = plane->translate( lineStart );
0147             QPointF transLineEnd = plane->translate( lineEnd );
0148             if ( ISNAN( transLineStart.x() ) || ISNAN( transLineStart.y() ) ||
0149                  ISNAN( transLineEnd.x() ) || ISNAN( transLineEnd.y() ) ) {
0150                 // ### can we catch NaN problems earlier, wasting fewer cycles?
0151                 continue;
0152             }
0153             if ( it.position() == 0.0 && drawZeroLine ) {
0154                 p->setPen( PrintingParameters::scalePen( gridAttrsX.zeroLinePen() ) );
0155             } else if ( it.type() == TickIterator::MinorTick ) {
0156                 p->setPen( PrintingParameters::scalePen( gridAttrs.subGridPen() ) );
0157             } else {
0158                 p->setPen( PrintingParameters::scalePen( gridAttrs.gridPen() ) );
0159             }
0160             p->drawLine( transLineStart, transLineEnd );
0161         }
0162     }
0163 }
0164 
0165 
0166 DataDimensionsList CartesianGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const
0167 {
0168     Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid",
0169                  "Error: calculateGrid() expects a list with exactly two entries." );
0170 
0171     CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( mPlane );
0172     Q_ASSERT_X ( plane, "CartesianGrid::calculateGrid",
0173                  "Error: PaintContext::calculatePlane() called, but no cartesian plane set." );
0174 
0175     DataDimensionsList l( rawDataDimensions );
0176 #if 0
0177     qDebug() << Q_FUNC_INFO << "initial grid X-range:" << l.first().start << "->" << l.first().end
0178              << "   substep width:" << l.first().subStepWidth;
0179     qDebug() << Q_FUNC_INFO << "initial grid Y-range:" << l.last().start << "->" << l.last().end
0180              << "   substep width:" << l.last().subStepWidth;
0181 #endif
0182     // rule:  Returned list is either empty, or it is providing two
0183     //        valid dimensions, complete with two non-Zero step widths.
0184     if ( isBoundariesValid( l ) ) {
0185         const QPointF translatedBottomLeft( plane->translateBack( plane->geometry().bottomLeft() ) );
0186         const QPointF translatedTopRight( plane->translateBack( plane->geometry().topRight() ) );
0187 
0188         const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
0189         const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
0190 
0191         const DataDimension dimX
0192                 = calculateGridXY( l.first(), Qt::Horizontal,
0193                                    gridAttrsX.adjustLowerBoundToGrid(),
0194                                    gridAttrsX.adjustUpperBoundToGrid() );
0195         if ( dimX.stepWidth ) {
0196             //qDebug("CartesianGrid::calculateGrid()   l.last().start:  %f   l.last().end:  %f", l.last().start, l.last().end);
0197             //qDebug("                                 l.first().start: %f   l.first().end: %f", l.first().start, l.first().end);
0198 
0199             // one time for the min/max value
0200             const DataDimension minMaxY
0201                     = calculateGridXY( l.last(), Qt::Vertical,
0202                                        gridAttrsY.adjustLowerBoundToGrid(),
0203                                        gridAttrsY.adjustUpperBoundToGrid() );
0204 
0205             if ( plane->autoAdjustGridToZoom()
0206                 && plane->axesCalcModeY() == CartesianCoordinatePlane::Linear
0207                 && plane->zoomFactorY() > 1.0 )
0208             {
0209                 l.last().start = translatedBottomLeft.y();
0210                 l.last().end   = translatedTopRight.y();
0211             }
0212             // and one other time for the step width
0213             const DataDimension dimY
0214                     = calculateGridXY( l.last(), Qt::Vertical,
0215                                        gridAttrsY.adjustLowerBoundToGrid(),
0216                                        gridAttrsY.adjustUpperBoundToGrid() );
0217             if ( dimY.stepWidth ) {
0218                 l.first().start        = dimX.start;
0219                 l.first().end          = dimX.end;
0220                 l.first().stepWidth    = dimX.stepWidth;
0221                 l.first().subStepWidth = dimX.subStepWidth;
0222                 l.last().start        = minMaxY.start;
0223                 l.last().end          = minMaxY.end;
0224                 l.last().stepWidth    = dimY.stepWidth;
0225                 l.last().subStepWidth    = dimY.subStepWidth;
0226                 //qDebug() << "CartesianGrid::calculateGrid()  final grid y-range:" << l.last().end - l.last().start << "   step width:" << l.last().stepWidth << endl;
0227                 // calculate some reasonable subSteps if the
0228                 // user did not set the sub grid but did set
0229                 // the stepWidth.
0230                 
0231                 // FIXME (Johannes)
0232                 // the last (y) dimension is not always the dimension for the ordinate!
0233                 // since there's no way to check for the orientation of this dimension here,
0234                 // we cannot automatically assume substep values
0235                 //if ( dimY.subStepWidth == 0 )
0236                 //    l.last().subStepWidth = dimY.stepWidth/2;
0237                 //else
0238                 //    l.last().subStepWidth = dimY.subStepWidth;
0239             }
0240         }
0241     }
0242 #if 0
0243     qDebug() << Q_FUNC_INFO << "final grid X-range:" << l.first().start << "->" << l.first().end
0244              << "   substep width:" << l.first().subStepWidth;
0245     qDebug() << Q_FUNC_INFO << "final grid Y-range:" << l.last().start << "->" << l.last().end
0246              << "   substep width:" << l.last().subStepWidth;
0247 #endif
0248     return l;
0249 }
0250 
0251 qreal fastPow10( int x )
0252 {
0253     qreal res = 1.0;
0254     if ( 0 <= x ) {
0255         for ( int i = 1; i <= x; ++i )
0256             res *= 10.0;
0257     } else {
0258         for ( int i = -1; i >= x; --i )
0259             res *= 0.1;
0260     }
0261     return res;
0262 }
0263 
0264 #ifdef Q_OS_WIN
0265 #define trunc(x) ((int)(x))
0266 #endif
0267 
0268 DataDimension CartesianGrid::calculateGridXY(
0269     const DataDimension& rawDataDimension,
0270     Qt::Orientation orientation,
0271     bool adjustLower, bool adjustUpper ) const
0272 {
0273     CartesianCoordinatePlane* const plane = dynamic_cast<CartesianCoordinatePlane*>( mPlane );
0274     if ( ( orientation == Qt::Vertical && plane->autoAdjustVerticalRangeToData() >= 100 ) ||
0275          ( orientation == Qt::Horizontal && plane->autoAdjustHorizontalRangeToData() >= 100 ) ) {
0276         adjustLower = false;
0277         adjustUpper = false;
0278     }
0279 
0280     DataDimension dim( rawDataDimension );
0281     if ( dim.isCalculated && dim.start != dim.end ) {
0282         if ( dim.calcMode == AbstractCoordinatePlane::Linear ) {
0283             // linear ( == not-logarithmic) calculation
0284             if ( dim.stepWidth == 0.0 ) {
0285                 QList<qreal> granularities;
0286                 switch ( dim.sequence ) {
0287                     case KChartEnums::GranularitySequence_10_20:
0288                         granularities << 1.0 << 2.0;
0289                         break;
0290                     case KChartEnums::GranularitySequence_10_50:
0291                         granularities << 1.0 << 5.0;
0292                         break;
0293                     case KChartEnums::GranularitySequence_25_50:
0294                         granularities << 2.5 << 5.0;
0295                         break;
0296                     case KChartEnums::GranularitySequence_125_25:
0297                         granularities << 1.25 << 2.5;
0298                         break;
0299                     case KChartEnums::GranularitySequenceIrregular:
0300                         granularities << 1.0 << 1.25 << 2.0 << 2.5 << 5.0;
0301                         break;
0302                 }
0303                 //qDebug("CartesianGrid::calculateGridXY()   dim.start: %f   dim.end: %f", dim.start, dim.end);
0304                 calculateStepWidth(
0305                     dim.start, dim.end, granularities, orientation,
0306                     dim.stepWidth, dim.subStepWidth,
0307                     adjustLower, adjustUpper );
0308             }
0309             // if needed, adjust start/end to match the step width:
0310             //qDebug() << "CartesianGrid::calculateGridXY() has 1st linear range: min " << dim.start << " and max" << dim.end;
0311 
0312             AbstractGrid::adjustLowerUpperRange( dim.start, dim.end, dim.stepWidth,
0313                     adjustLower, adjustUpper );
0314             //qDebug() << "CartesianGrid::calculateGridXY() returns linear range: min " << dim.start << " and max" << dim.end;
0315         } else {
0316             // logarithmic calculation with negative values
0317             if ( dim.end <= 0 )
0318             {
0319                 qreal min;
0320                 const qreal minRaw = qMin( dim.start, dim.end );
0321                 const int minLog = -static_cast<int>(trunc( log10( -minRaw ) ) );
0322                 if ( minLog >= 0 )
0323                     min = qMin( minRaw, -std::numeric_limits< qreal >::epsilon() );
0324                 else
0325                     min = -fastPow10( -(minLog-1) );
0326             
0327                 qreal max;
0328                 const qreal maxRaw = qMin( -std::numeric_limits< qreal >::epsilon(), qMax( dim.start, dim.end ) );
0329                 const int maxLog = -static_cast<int>(ceil( log10( -maxRaw ) ) );
0330                 if ( maxLog >= 0 )
0331                     max = -1;
0332                 else if ( fastPow10( -maxLog ) < maxRaw )
0333                     max = -fastPow10( -(maxLog+1) );
0334                 else
0335                     max = -fastPow10( -maxLog );
0336                 if ( adjustLower )
0337                     dim.start = min;
0338                 if ( adjustUpper )
0339                     dim.end   = max;
0340                 dim.stepWidth = -pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
0341             }
0342             // logarithmic calculation (ignoring all negative values)
0343             else
0344             {
0345                 qreal min;
0346                 const qreal minRaw = qMax( qMin( dim.start, dim.end ), qreal( 0.0 ) );
0347                 const int minLog = static_cast<int>(trunc( log10( minRaw ) ) );
0348                 if ( minLog <= 0 && dim.end < 1.0 )
0349                     min = qMax( minRaw, std::numeric_limits< qreal >::epsilon() );
0350                 else if ( minLog <= 0 )
0351                     min = qMax( qreal(0.00001), dim.start );
0352                 else
0353                     min = fastPow10( minLog-1 );
0354 
0355                 // Uh oh. Logarithmic scaling doesn't work with a lower or upper
0356                 // bound being 0.
0357                 const bool zeroBound = dim.start == 0.0 || dim.end == 0.0;
0358 
0359                 qreal max;
0360                 const qreal maxRaw = qMax( qMax( dim.start, dim.end ), qreal( 0.0 ) );
0361                 const int maxLog = static_cast<int>(ceil( log10( maxRaw ) ) );
0362                 if ( maxLog <= 0 )
0363                     max = 1;
0364                 else if ( fastPow10( maxLog ) < maxRaw )
0365                     max = fastPow10( maxLog+1 );
0366                 else
0367                     max = fastPow10( maxLog );
0368                 if ( adjustLower || zeroBound )
0369                     dim.start = min;
0370                 if ( adjustUpper || zeroBound )
0371                     dim.end   = max;
0372                 dim.stepWidth = pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
0373             }
0374         }
0375     } else {
0376         //qDebug() << "CartesianGrid::calculateGridXY() returns stepWidth 1.0  !!";
0377         // Do not ignore the user configuration
0378         dim.stepWidth = dim.stepWidth ? dim.stepWidth : 1.0;
0379     }
0380     return dim;
0381 }
0382 
0383 
0384 static void calculateSteps(
0385     qreal start_, qreal end_, const QList<qreal>& list,
0386     int minSteps, int maxSteps,
0387     int power,
0388     qreal& steps, qreal& stepWidth,
0389     bool adjustLower, bool adjustUpper )
0390 {
0391     //qDebug("-----------------------------------\nstart: %f   end: %f   power-of-ten: %i", start_, end_, power);
0392 
0393     qreal distance = 0.0;
0394     steps = 0.0;
0395 
0396     const int lastIdx = list.count()-1;
0397     for ( int i = 0;  i <= lastIdx;  ++i ) {
0398         const qreal testStepWidth = list.at(lastIdx - i) * fastPow10( power );
0399         //qDebug( "testing step width: %f", testStepWidth);
0400         qreal start = qMin( start_, end_ );
0401         qreal end   = qMax( start_, end_ );
0402         //qDebug("pre adjusting    start: %f   end: %f", start, end);
0403         AbstractGrid::adjustLowerUpperRange( start, end, testStepWidth, adjustLower, adjustUpper );
0404         //qDebug("post adjusting   start: %f   end: %f", start, end);
0405 
0406         const qreal testDistance = qAbs(end - start);
0407         const qreal testSteps    = testDistance / testStepWidth;
0408 
0409         //qDebug() << "testDistance:" << testDistance << "  distance:" << distance;
0410         if ( (minSteps <= testSteps) && (testSteps <= maxSteps)
0411               && ( (steps == 0.0) || (testDistance <= distance) ) ) {
0412             steps     = testSteps;
0413             stepWidth = testStepWidth;
0414             distance  = testDistance;
0415             //qDebug( "start: %f   end: %f   step width: %f   steps: %f   distance: %f", start, end, stepWidth, steps, distance);
0416         }
0417     }
0418 }
0419 
0420 
0421 void CartesianGrid::calculateStepWidth(
0422     qreal start_, qreal end_,
0423     const QList<qreal>& granularities,
0424     Qt::Orientation orientation,
0425     qreal& stepWidth, qreal& subStepWidth,
0426     bool adjustLower, bool adjustUpper ) const
0427 {
0428     Q_UNUSED( orientation );
0429 
0430     Q_ASSERT_X ( granularities.count(), "CartesianGrid::calculateStepWidth",
0431                  "Error: The list of GranularitySequence values is empty." );
0432     QList<qreal> list( granularities );
0433     std::sort(list.begin(), list.end());
0434 
0435     const qreal start = qMin( start_, end_);
0436     const qreal end   = qMax( start_, end_);
0437     const qreal distance = end - start;
0438     //qDebug( "raw data start: %f   end: %f", start, end);
0439 
0440     qreal steps;
0441     int power = 0;
0442     while ( list.last() * fastPow10( power ) < distance ) {
0443         ++power;
0444     };
0445     // We have the sequence *two* times in the calculation test list,
0446     // so we will be sure to find the best match:
0447     const int count = list.count();
0448     QList<qreal> testList;
0449 
0450     for ( int dec = -1; dec == -1 || fastPow10( dec + 1 ) >= distance; --dec )
0451         for ( int i = 0;  i < count;  ++i )
0452             testList << list.at(i) * fastPow10( dec );
0453 
0454     testList << list;
0455 
0456     do {
0457         calculateSteps( start, end, testList, m_minsteps, m_maxsteps, power,
0458                         steps, stepWidth,
0459                         adjustLower, adjustUpper );
0460         --power;
0461     } while ( steps == 0.0 );
0462     ++power;
0463     //qDebug( "steps calculated:  stepWidth: %f   steps: %f", stepWidth, steps);
0464 
0465     // find the matching sub-grid line width in case it is
0466     // not set by the user
0467 
0468     if ( subStepWidth == 0.0 ) {
0469         if ( stepWidth == list.first() * fastPow10( power ) ) {
0470             subStepWidth = list.last() * fastPow10( power-1 );
0471             //qDebug("A");
0472         } else if ( stepWidth == list.first() * fastPow10( power-1 ) ) {
0473             subStepWidth = list.last() * fastPow10( power-2 );
0474             //qDebug("B");
0475         } else {
0476             qreal smallerStepWidth = list.first();
0477             for ( int i = 1;  i < list.count();  ++i ) {
0478                 if ( stepWidth == list.at( i ) * fastPow10( power ) ) {
0479                     subStepWidth = smallerStepWidth * fastPow10( power );
0480                     break;
0481                 }
0482                 if ( stepWidth == list.at( i ) * fastPow10( power-1 ) ) {
0483                     subStepWidth = smallerStepWidth * fastPow10( power-1 );
0484                     break;
0485                 }
0486                 smallerStepWidth = list.at( i );
0487             }
0488         }
0489     }
0490     //qDebug("CartesianGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth);
0491 }