KD Chart 2  [rev.2.5.1]
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
KDChartCartesianAxis.cpp
Go to the documentation of this file.
1 /****************************************************************************
2 ** Copyright (C) 2001-2013 Klaralvdalens Datakonsult AB. All rights reserved.
3 **
4 ** This file is part of the KD Chart library.
5 **
6 ** Licensees holding valid commercial KD Chart licenses may use this file in
7 ** accordance with the KD Chart Commercial License Agreement provided with
8 ** the Software.
9 **
10 **
11 ** This file may be distributed and/or modified under the terms of the
12 ** GNU General Public License version 2 and version 3 as published by the
13 ** Free Software Foundation and appearing in the file LICENSE.GPL.txt included.
14 **
15 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
16 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
17 **
18 ** Contact info@kdab.com if any conditions of this licensing are not
19 ** clear to you.
20 **
21 **********************************************************************/
22 
23 #include "KDChartCartesianAxis.h"
24 #include "KDChartCartesianAxis_p.h"
25 
26 #include <cmath>
27 
28 #include <QtDebug>
29 #include <QPainter>
30 #include <QPen>
31 #include <QBrush>
32 #include <QApplication>
33 
34 #include "KDChartPaintContext.h"
35 #include "KDChartChart.h"
37 #include "KDChartAbstractDiagram_p.h"
38 #include "KDChartAbstractGrid.h"
39 #include "KDChartPainterSaver_p.h"
40 #include "KDChartLayoutItems.h"
41 #include "KDChartBarDiagram.h"
42 #include "KDChartStockDiagram.h"
43 #include "KDChartLineDiagram.h"
45 
46 #include <KDABLibFakes>
47 
48 using namespace KDChart;
49 
50 #define d (d_func())
51 
52 static qreal slightlyLessThan( qreal r )
53 {
54  if ( r == 0.0 ) {
55  // scale down the epsilon somewhat arbitrarily
56  return r - std::numeric_limits< qreal >::epsilon() * 1e-6;
57  }
58  // scale the epsilon so that it (hopefully) changes at least the least significant bit of r
59  qreal diff = qAbs( r ) * std::numeric_limits< qreal >::epsilon() * 2.0;
60  return r - diff;
61 }
62 
63 static int numSignificantDecimalPlaces( qreal floatNumber )
64 {
65  static const int maxPlaces = 15;
66  QString sample = QString::number( floatNumber, 'f', maxPlaces ).section( QLatin1Char('.'), 1, 2 );
67  int ret = maxPlaces;
68  for ( ; ret > 0; ret-- ) {
69  if ( sample[ ret - 1 ] != QLatin1Char( '0' ) ) {
70  break;
71  }
72  }
73  return ret;
74 }
75 
76 TickIterator::TickIterator( CartesianAxis* a, CartesianCoordinatePlane* plane, uint majorThinningFactor,
77  bool omitLastTick )
78  : m_axis( a ),
79  m_majorThinningFactor( majorThinningFactor ),
80  m_majorLabelCount( 0 ),
81  m_type( NoTick )
82 {
83  // deal with the things that are specfic to axes (like annotations), before the generic init().
84  XySwitch xy( m_axis->d_func()->isVertical() );
85  m_dimension = xy( plane->gridDimensionsList().first(), plane->gridDimensionsList().last() );
86  if ( omitLastTick ) {
87  // In bar and stock charts the last X tick is a fencepost with no associated value, which is
88  // convenient for grid painting. Here we have to manually exclude it to avoid overpainting.
89  m_dimension.end -= m_dimension.stepWidth;
90  }
91 
92  m_annotations = m_axis->d_func()->annotations;
93  m_customTicks = m_axis->d_func()->customTicksPositions;
94 
95  const qreal inf = std::numeric_limits< qreal >::infinity();
96 
97  if ( m_customTicks.count() ) {
98  qSort( m_customTicks.begin(), m_customTicks.end() );
99  m_customTickIndex = 0;
100  m_customTick = m_customTicks.at( m_customTickIndex );
101  } else {
102  m_customTickIndex = -1;
103  m_customTick = inf;
104  }
105 
106  if ( m_majorThinningFactor > 1 && hasShorterLabels() ) {
107  m_manualLabelTexts = m_axis->shortLabels();
108  } else {
109  m_manualLabelTexts = m_axis->labels();
110  }
111  m_manualLabelIndex = m_manualLabelTexts.isEmpty() ? -1 : 0;
112 
113  if ( !m_dimension.isCalculated ) {
114  // ### depending on the data, it is difficult to impossible to choose anchors (where ticks
115  // corresponding to the header labels are) on the ordinate or even the abscissa with
116  // 2-dimensional data. this should be somewhat mitigated by isCalculated only being false
117  // when header data labels should work, at least that seems to be what the code that sets up
118  // the dimensions is trying to do.
119  QStringList dataHeaderLabels;
120  AbstractDiagram* const dia = plane->diagram();
121  dataHeaderLabels = dia->itemRowLabels();
122  if ( !dataHeaderLabels.isEmpty() ) {
123  AttributesModel* model = dia->attributesModel();
124  const int anchorCount = model->rowCount( QModelIndex() );
125  if ( anchorCount == dataHeaderLabels.count() ) {
126  for ( int i = 0; i < anchorCount; i++ ) {
127  // ### ordinal number as anchor point generally only works for 1-dimensional data
128  m_dataHeaderLabels.insert( qreal( i ), dataHeaderLabels.at( i ) );
129  }
130  }
131  }
132  }
133 
134  bool hasMajorTicks = m_axis->rulerAttributes().showMajorTickMarks();
135  bool hasMinorTicks = m_axis->rulerAttributes().showMinorTickMarks();
136 
137  init( xy.isY, hasMajorTicks, hasMinorTicks, plane );
138 }
139 
140 TickIterator::TickIterator( bool isY, const DataDimension& dimension, bool hasMajorTicks, bool hasMinorTicks,
141  CartesianCoordinatePlane* plane, uint majorThinningFactor )
142  : m_axis( 0 ),
143  m_dimension( dimension ),
144  m_majorThinningFactor( majorThinningFactor ),
145  m_majorLabelCount( 0 ),
146  m_customTickIndex( -1 ),
147  m_manualLabelIndex( -1 ),
148  m_type( NoTick ),
149  m_customTick( std::numeric_limits< qreal >::infinity() )
150 {
151  init( isY, hasMajorTicks, hasMinorTicks, plane );
152 }
153 
154 void TickIterator::init( bool isY, bool hasMajorTicks, bool hasMinorTicks,
155  CartesianCoordinatePlane* plane )
156 {
157  Q_ASSERT( std::numeric_limits< qreal >::has_infinity );
158 
159  m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
160  // sanity check against infinite loops
161  hasMajorTicks = hasMajorTicks && ( m_dimension.stepWidth > 0 || m_isLogarithmic );
162  hasMinorTicks = hasMinorTicks && ( m_dimension.subStepWidth > 0 || m_isLogarithmic );
163 
164  XySwitch xy( isY );
165 
166  GridAttributes gridAttributes = plane->gridAttributes( xy( Qt::Horizontal, Qt::Vertical ) );
167  m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
168  if ( !m_isLogarithmic ) {
169  // adjustedLowerUpperRange() is intended for use with linear scaling; specifically it would
170  // round lower bounds < 1 to 0.
171 
172  const bool fixedRange = xy( plane->autoAdjustHorizontalRangeToData(),
173  plane->autoAdjustVerticalRangeToData() ) >= 100;
174  const bool adjustLower = gridAttributes.adjustLowerBoundToGrid() && !fixedRange;
175  const bool adjustUpper = gridAttributes.adjustUpperBoundToGrid() && !fixedRange;
176  m_dimension = AbstractGrid::adjustedLowerUpperRange( m_dimension, adjustLower, adjustUpper );
177 
178  m_decimalPlaces = numSignificantDecimalPlaces( m_dimension.stepWidth );
179  } else {
180  // the number of significant decimal places for each label naturally varies with logarithmic scaling
181  m_decimalPlaces = -1;
182  }
183 
184  const qreal inf = std::numeric_limits< qreal >::infinity();
185 
186  // try to place m_position just in front of the first tick to be drawn so that operator++()
187  // can be used to find the first tick
188  if ( m_isLogarithmic ) {
189  if ( ISNAN( m_dimension.start ) || ISNAN( m_dimension.end ) ) {
190  // this can happen in a spurious paint operation before everything is set up;
191  // just bail out to avoid an infinite loop in that case.
192  m_dimension.start = 0.0;
193  m_dimension.end = 0.0;
194  m_position = inf;
195  m_majorTick = inf;
196  m_minorTick = inf;
197  } else if ( m_dimension.start >= 0 ) {
198  m_position = m_dimension.start ? pow( 10.0, floor( log10( m_dimension.start ) ) - 1.0 )
199  : 1e-6;
200  m_majorTick = hasMajorTicks ? m_position : inf;
201  m_minorTick = hasMinorTicks ? m_position * 20.0 : inf;
202  } else {
203  m_position = -pow( 10.0, ceil( log10( -m_dimension.start ) ) + 1.0 );
204  m_majorTick = hasMajorTicks ? m_position : inf;
205  m_minorTick = hasMinorTicks ? m_position * 0.09 : inf;
206  }
207  } else {
208  m_majorTick = hasMajorTicks ? m_dimension.start : inf;
209  m_minorTick = hasMinorTicks ? m_dimension.start : inf;
210  m_position = slightlyLessThan( m_dimension.start );
211  }
212 
213  ++( *this );
214 }
215 
216 bool TickIterator::areAlmostEqual( qreal r1, qreal r2 ) const
217 {
218  if ( !m_isLogarithmic ) {
219  return qAbs( r2 - r1 ) < ( m_dimension.end - m_dimension.start ) * 1e-6;
220  } else {
221  return qAbs( r2 - r1 ) < qMax( qAbs( r1 ), qAbs( r2 ) ) * 0.01;
222  }
223 }
224 
225 bool TickIterator::isHigherPrecedence( qreal importantTick, qreal unimportantTick ) const
226 {
227  return importantTick != std::numeric_limits< qreal >::infinity() &&
228  ( importantTick <= unimportantTick || areAlmostEqual( importantTick, unimportantTick ) );
229 }
230 
231 void TickIterator::computeMajorTickLabel( int decimalPlaces )
232 {
233  if ( m_manualLabelIndex >= 0 ) {
234  m_text = m_manualLabelTexts[ m_manualLabelIndex++ ];
235  if ( m_manualLabelIndex >= m_manualLabelTexts.count() ) {
236  // manual label texts repeat if there are less label texts than ticks on an axis
237  m_manualLabelIndex = 0;
238  }
239  m_type = m_majorThinningFactor > 1 ? MajorTickManualShort : MajorTickManualLong;
240  } else {
241  // if m_axis is null, we are dealing with grid lines. grid lines never need labels.
242  if ( m_axis && ( m_majorLabelCount++ % m_majorThinningFactor ) == 0 ) {
244  m_dataHeaderLabels.lowerBound( slightlyLessThan( m_position ) );
245 
246  if ( it != m_dataHeaderLabels.constEnd() && areAlmostEqual( it.key(), m_position ) ) {
247  m_text = it.value();
248  m_type = MajorTickHeaderDataLabel;
249  } else {
250  // 'f' to avoid exponential notation for large numbers, consistent with data value text
251  if ( decimalPlaces < 0 ) {
252  decimalPlaces = numSignificantDecimalPlaces( m_position );
253  }
254  m_text = QString::number( m_position, 'f', decimalPlaces );
255  m_type = MajorTick;
256  }
257  } else {
258  m_text.clear();
259  m_type = MajorTick;
260  }
261  }
262 }
263 
264 void TickIterator::operator++()
265 {
266  if ( isAtEnd() ) {
267  return;
268  }
269  const qreal inf = std::numeric_limits< qreal >::infinity();
270 
271  // make sure to find the next tick at a value strictly greater than m_position
272 
273  if ( !m_annotations.isEmpty() ) {
274  QMap< qreal, QString >::ConstIterator it = m_annotations.upperBound( m_position );
275  if ( it != m_annotations.constEnd() ) {
276  m_position = it.key();
277  m_text = it.value();
278  m_type = CustomTick;
279  } else {
280  m_position = inf;
281  }
282  } else if (m_dimension.start == m_dimension.end) {
283  // bail out to avoid KDCH-967 and possibly other problems: an empty range is not necessarily easy to
284  // iterate over, so don't try to "properly" determine the next tick which is certain to be out of the
285  // given range - just trivially skip to the end.
286  m_position = inf;
287  } else {
288  // advance the calculated ticks
289  if ( m_isLogarithmic ) {
290  while ( m_majorTick <= m_position ) {
291  m_majorTick *= m_position >= 0 ? 10 : 0.1;
292  }
293  while ( m_minorTick <= m_position ) {
294  // the next major tick position should be greater than this
295  m_minorTick += m_majorTick * ( m_position >= 0 ? 0.1 : 1.0 );
296  }
297  } else {
298  while ( m_majorTick <= m_position ) {
299  m_majorTick += m_dimension.stepWidth;
300  }
301  while ( m_minorTick <= m_position ) {
302  m_minorTick += m_dimension.subStepWidth;
303  }
304  }
305 
306  while ( m_customTickIndex >= 0 && m_customTick <= m_position ) {
307  if ( ++m_customTickIndex >= m_customTicks.count() ) {
308  m_customTickIndex = -1;
309  m_customTick = inf;
310  break;
311  }
312  m_customTick = m_customTicks.at( m_customTickIndex );
313  }
314 
315  // now see which kind of tick we'll have
316  if ( isHigherPrecedence( m_customTick, m_majorTick ) && isHigherPrecedence( m_customTick, m_minorTick ) ) {
317  m_position = m_customTick;
318  computeMajorTickLabel( -1 );
319  // override the MajorTick type here because those tick's labels are collision-tested, which we don't want
320  // for custom ticks. they may be arbitrarily close to other ticks, causing excessive label thinning.
321  if ( m_type == MajorTick ) {
322  m_type = CustomTick;
323  }
324  } else if ( isHigherPrecedence( m_majorTick, m_minorTick ) ) {
325  m_position = m_majorTick;
326  if ( m_minorTick != inf ) {
327  // realign minor to major
328  m_minorTick = m_majorTick;
329  }
330  computeMajorTickLabel( m_decimalPlaces );
331  } else if ( m_minorTick != inf ) {
332  m_position = m_minorTick;
333  m_text.clear();
334  m_type = MinorTick;
335  } else {
336  m_position = inf;
337  }
338  }
339 
340  if ( m_position > m_dimension.end || ISNAN( m_position ) ) {
341  m_position = inf; // make isAtEnd() return true
342  m_text.clear();
343  m_type = NoTick;
344  }
345 }
346 
347 CartesianAxis::CartesianAxis( AbstractCartesianDiagram* diagram )
348  : AbstractAxis ( new Private( diagram, this ), diagram )
349 {
350  init();
351 }
352 
354 {
355  // when we remove the first axis it will unregister itself and
356  // propagate the next one to the primary, thus the while loop
357  while ( d->mDiagram ) {
358  AbstractCartesianDiagram *cd = qobject_cast< AbstractCartesianDiagram* >( d->mDiagram );
359  cd->takeAxis( this );
360  }
361  KDAB_FOREACH( AbstractDiagram *diagram, d->secondaryDiagrams ) {
363  cd->takeAxis( this );
364  }
365 }
366 
367 void CartesianAxis::init()
368 {
369  d->customTickLength = 3;
370  d->position = Bottom;
372  connect( this, SIGNAL( coordinateSystemChanged() ), SLOT( coordinateSystemChanged() ) );
373 }
374 
375 
376 bool CartesianAxis::compare( const CartesianAxis* other ) const
377 {
378  if ( other == this ) {
379  return true;
380  }
381  if ( !other ) {
382  return false;
383  }
384  return AbstractAxis::compare( other ) && ( position() == other->position() ) &&
385  ( titleText() == other->titleText() ) &&
386  ( titleTextAttributes() == other->titleTextAttributes() );
387 }
388 
389 void CartesianAxis::coordinateSystemChanged()
390 {
391  layoutPlanes();
392 }
393 
394 
395 void CartesianAxis::setTitleText( const QString& text )
396 {
397  d->titleText = text;
398  layoutPlanes();
399 }
400 
402 {
403  return d->titleText;
404 }
405 
407 {
408  d->titleTextAttributes = a;
409  d->useDefaultTextAttributes = false;
410  layoutPlanes();
411 }
412 
414 {
417  Measure me( ta.fontSize() );
418  me.setValue( me.value() * 1.5 );
419  ta.setFontSize( me );
420  return ta;
421  }
422  return d->titleTextAttributes;
423 }
424 
426 {
427  d->useDefaultTextAttributes = true;
428  layoutPlanes();
429 }
430 
432 {
433  return d->useDefaultTextAttributes;
434 }
435 
436 
438 {
439  d->position = p;
440  layoutPlanes();
441 }
442 
443 #if QT_VERSION < 0x040400 || defined(Q_COMPILER_MANGLES_RETURN_TYPE)
444 const
445 #endif
447 {
448  return d->position;
449 }
450 
452 {
453  if ( ! d->diagram() || ! d->diagram()->coordinatePlane() ) {
454  return;
455  }
456  AbstractCoordinatePlane* plane = d->diagram()->coordinatePlane();
457  if ( plane ) {
458  plane->layoutPlanes();
459  }
460 }
461 
462 static bool referenceDiagramIsBarDiagram( const AbstractDiagram * diagram )
463 {
464  const AbstractCartesianDiagram * dia =
465  qobject_cast< const AbstractCartesianDiagram * >( diagram );
466  if ( dia && dia->referenceDiagram() )
467  dia = dia->referenceDiagram();
468  return qobject_cast< const BarDiagram* >( dia ) != 0;
469 }
470 
472 {
473  const AbstractCartesianDiagram * dia =
474  qobject_cast< const AbstractCartesianDiagram * >( diagram );
475  if ( dia && dia->referenceDiagram() )
476  dia = dia->referenceDiagram();
477  if ( qobject_cast< const BarDiagram* >( dia ) )
478  return true;
479  if ( qobject_cast< const StockDiagram* >( dia ) )
480  return true;
481 
482  const LineDiagram * lineDiagram = qobject_cast< const LineDiagram* >( dia );
483  return lineDiagram && lineDiagram->centerDataPoints();
484 }
485 
487 {
488  const Qt::Orientation diagramOrientation = referenceDiagramIsBarDiagram( d->diagram() ) ? ( ( BarDiagram* )( d->diagram() ) )->orientation()
489  : Qt::Vertical;
490  return diagramOrientation == Qt::Vertical ? position() == Bottom || position() == Top
491  : position() == Left || position() == Right;
492 }
493 
495 {
496  return !isAbscissa();
497 }
498 
499 void CartesianAxis::paint( QPainter* painter )
500 {
501  if ( !d->diagram() || !d->diagram()->coordinatePlane() ) {
502  return;
503  }
504  PaintContext ctx;
505  ctx.setPainter ( painter );
506  AbstractCoordinatePlane *const plane = d->diagram()->coordinatePlane();
507  ctx.setCoordinatePlane( plane );
508 
509  ctx.setRectangle( QRectF( areaGeometry() ) );
510  PainterSaver painterSaver( painter );
511 
512  // enable clipping only when required due to zoom, because it slows down painting
513  // (the alternative to clipping when zoomed in requires much more work to paint just the right area)
514  const qreal zoomFactor = d->isVertical() ? plane->zoomFactorY() : plane->zoomFactorX();
515  if ( zoomFactor > 1.0 ) {
516  painter->setClipRegion( areaGeometry().adjusted( - d->amountOfLeftOverlap - 1, - d->amountOfTopOverlap - 1,
517  d->amountOfRightOverlap + 1, d->amountOfBottomOverlap + 1 ) );
518  }
519  paintCtx( &ctx );
520 }
521 
522 const TextAttributes CartesianAxis::Private::titleTextAttributesWithAdjustedRotation() const
523 {
524  TextAttributes titleTA( titleTextAttributes );
525  int rotation = titleTA.rotation();
526  if ( position == Left || position == Right ) {
527  rotation += 270;
528  }
529  if ( rotation >= 360 ) {
530  rotation -= 360;
531  }
532  // limit the allowed values to 0, 90, 180, 270
533  rotation = ( rotation / 90 ) * 90;
534  titleTA.setRotation( rotation );
535  return titleTA;
536 }
537 
538 QString CartesianAxis::Private::customizedLabelText( const QString& text, Qt::Orientation orientation,
539  qreal value ) const
540 {
541  // ### like in the old code, using int( value ) as column number...
542  QString withUnits = diagram()->unitPrefix( int( value ), orientation, true ) +
543  text +
544  diagram()->unitSuffix( int( value ), orientation, true );
545  return axis()->customizedLabel( withUnits );
546 }
547 
548 void CartesianAxis::setTitleSpace( qreal axisTitleSpace )
549 {
550  d->axisTitleSpace = axisTitleSpace;
551 }
552 
554 {
555  return d->axisTitleSpace;
556 }
557 
558 void CartesianAxis::setTitleSize( qreal value )
559 {
560  d->axisSize = value;
561 }
562 
564 {
565  return d->axisSize;
566 }
567 
568 void CartesianAxis::Private::drawTitleText( QPainter* painter, CartesianCoordinatePlane* plane,
569  const QRect& geoRect ) const
570 {
571  const TextAttributes titleTA( titleTextAttributesWithAdjustedRotation() );
572  if ( titleTA.isVisible() ) {
573  TextLayoutItem titleItem( titleText, titleTA, plane->parent(), KDChartEnums::MeasureOrientationMinimum,
574  Qt::AlignHCenter | Qt::AlignVCenter );
575  QPointF point;
576  QSize size = titleItem.sizeHint();
577  //FIXME(khz): We definitely need to provide a way that users can decide
578  // the position of an axis title.
579  switch ( position ) {
580  case Top:
581  point.setX( geoRect.left() + geoRect.width() / 2 );
582  point.setY( geoRect.top() + ( size.height() / 2 ) / axisTitleSpace );
583  size.setWidth( qMin( size.width(), axis()->geometry().width() ) );
584  break;
585  case Bottom:
586  point.setX( geoRect.left() + geoRect.width() / 2 );
587  point.setY( geoRect.bottom() - ( size.height() / 2 ) / axisTitleSpace );
588  size.setWidth( qMin( size.width(), axis()->geometry().width() ) );
589  break;
590  case Left:
591  point.setX( geoRect.left() + ( size.width() / 2 ) / axisTitleSpace );
592  point.setY( geoRect.top() + geoRect.height() / 2 );
593  size.setHeight( qMin( size.height(), axis()->geometry().height() ) );
594  break;
595  case Right:
596  point.setX( geoRect.right() - ( size.width() / 2 ) / axisTitleSpace );
597  point.setY( geoRect.top() + geoRect.height() / 2 );
598  size.setHeight( qMin( size.height(), axis()->geometry().height() ) );
599  break;
600  }
601  const PainterSaver painterSaver( painter );
602  painter->translate( point );
603  titleItem.setGeometry( QRect( QPoint( -size.width() / 2, -size.height() / 2 ), size ) );
604  titleItem.paint( painter );
605  }
606 }
607 
608 bool CartesianAxis::Private::isVertical() const
609 {
610  return axis()->isAbscissa() == AbstractDiagram::Private::get( diagram() )->isTransposed();
611 }
612 
614 {
615  Q_ASSERT_X ( d->diagram(), "CartesianAxis::paint",
616  "Function call not allowed: The axis is not assigned to any diagram." );
617 
618  CartesianCoordinatePlane* plane = dynamic_cast<CartesianCoordinatePlane*>( context->coordinatePlane() );
619  Q_ASSERT_X ( plane, "CartesianAxis::paint",
620  "Bad function call: PaintContext::coordinatePlane() NOT a cartesian plane." );
621 
622  // note: Not having any data model assigned is no bug
623  // but we can not draw an axis then either.
624  if ( !d->diagram()->model() ) {
625  return;
626  }
627 
628  const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( d->diagram() ) && isAbscissa();
629 
630  XySwitch geoXy( d->isVertical() );
631 
632  QPainter* const painter = context->painter();
633 
634  // determine the position of the axis (also required for labels) and paint it
635 
636  qreal transversePosition = signalingNaN; // in data space
637  // the next one describes an additional shift in screen space; it is unfortunately required to
638  // make axis sharing work, which uses the areaGeometry() to override the position of the axis.
639  qreal transverseScreenSpaceShift = signalingNaN;
640  {
641  // determine the unadulterated position in screen space
642 
643  DataDimension dimX = plane->gridDimensionsList().first();
644  DataDimension dimY = plane->gridDimensionsList().last();
645  QPointF start( dimX.start, dimY.start );
646  QPointF end( dimX.end, dimY.end );
647  // consider this: you can turn a diagonal line into a horizontal or vertical line on any
648  // edge by changing just one of its four coordinates.
649  switch ( position() ) {
651  end.setY( dimY.start );
652  break;
653  case CartesianAxis::Top:
654  start.setY( dimY.end );
655  break;
656  case CartesianAxis::Left:
657  end.setX( dimX.start );
658  break;
660  start.setX( dimX.end );
661  break;
662  }
663 
664  transversePosition = geoXy( start.y(), start.x() );
665 
666  QPointF transStart = plane->translate( start );
667  QPointF transEnd = plane->translate( end );
668 
669  // an externally set areaGeometry() moves the axis position transversally; the shift is
670  // nonzero only when this is a shared axis
671 
672  const QRect geo = areaGeometry();
673  switch ( position() ) {
675  transverseScreenSpaceShift = geo.top() - transStart.y();
676  break;
677  case CartesianAxis::Top:
678  transverseScreenSpaceShift = geo.bottom() - transStart.y();
679  break;
680  case CartesianAxis::Left:
681  transverseScreenSpaceShift = geo.right() - transStart.x();
682  break;
684  transverseScreenSpaceShift = geo.left() - transStart.x();
685  break;
686  }
687 
688  geoXy.lvalue( transStart.ry(), transStart.rx() ) += transverseScreenSpaceShift;
689  geoXy.lvalue( transEnd.ry(), transEnd.rx() ) += transverseScreenSpaceShift;
690 
691  if ( rulerAttributes().showRulerLine() ) {
692  bool clipSaved = context->painter()->hasClipping();
693  painter->setClipping( false );
694  painter->drawLine( transStart, transEnd );
695  painter->setClipping( clipSaved );
696  }
697  }
698 
699  // paint ticks and labels
700 
701  TextAttributes labelTA = textAttributes();
702  RulerAttributes rulerAttr = rulerAttributes();
703 
704  int labelThinningFactor = 1;
705  // TODO: label thinning also when grid line distance < 4 pixels, not only when labels collide
706  TextLayoutItem *tickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(),
708  TextLayoutItem *prevTickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(),
710  QPointF prevTickLabelPos;
711  enum {
712  Layout = 0,
713  Painting,
714  Done
715  };
716  for ( int step = labelTA.isVisible() ? Layout : Painting; step < Done; step++ ) {
717  bool skipFirstTick = !rulerAttr.showFirstTick();
718  bool isFirstLabel = true;
719  for ( TickIterator it( this, plane, labelThinningFactor, centerTicks ); !it.isAtEnd(); ++it ) {
720  if ( skipFirstTick ) {
721  skipFirstTick = false;
722  continue;
723  }
724 
725  const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. );
726  QPointF onAxis = plane->translate( geoXy( QPointF( drawPos, transversePosition ) ,
727  QPointF( transversePosition, drawPos ) ) );
728  geoXy.lvalue( onAxis.ry(), onAxis.rx() ) += transverseScreenSpaceShift;
729  const bool isOutwardsPositive = position() == Bottom || position() == Right;
730 
731  // paint the tick mark
732 
733  QPointF tickEnd = onAxis;
734  qreal tickLen = it.type() == TickIterator::CustomTick ?
735  d->customTickLength : tickLength( it.type() == TickIterator::MinorTick );
736  geoXy.lvalue( tickEnd.ry(), tickEnd.rx() ) += isOutwardsPositive ? tickLen : -tickLen;
737 
738  // those adjustments are required to paint the ticks exactly on the axis and of the right length
739  if ( position() == Top ) {
740  onAxis.ry() += 1;
741  tickEnd.ry() += 1;
742  } else if ( position() == Left ) {
743  tickEnd.rx() += 1;
744  }
745 
746  if ( step == Painting ) {
747  painter->save();
748  if ( rulerAttr.hasTickMarkPenAt( it.position() ) ) {
749  painter->setPen( rulerAttr.tickMarkPen( it.position() ) );
750  } else {
751  painter->setPen( it.type() == TickIterator::MinorTick ? rulerAttr.minorTickMarkPen()
752  : rulerAttr.majorTickMarkPen() );
753  }
754  painter->drawLine( onAxis, tickEnd );
755  painter->restore();
756  }
757 
758  if ( it.text().isEmpty() || !labelTA.isVisible() ) {
759  // the following code in the loop is only label painting, so skip it
760  continue;
761  }
762 
763  // paint the label
764 
765  QString text = it.text();
766  if ( it.type() == TickIterator::MajorTick ) {
767  text = d->customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() );
768  }
769  tickLabel->setText( text );
770  QSizeF size = QSizeF( tickLabel->sizeHint() );
771  QPolygon labelPoly = tickLabel->boundingPolygon();
772  Q_ASSERT( labelPoly.count() == 4 );
773 
774  // for alignment, find the label polygon edge "most parallel" and closest to the axis
775 
776  int axisAngle = 0;
777  switch ( position() ) {
778  case Bottom:
779  axisAngle = 0; break;
780  case Top:
781  axisAngle = 180; break;
782  case Right:
783  axisAngle = 270; break;
784  case Left:
785  axisAngle = 90; break;
786  default:
787  Q_ASSERT( false );
788  }
789  // the left axis is not actually pointing down and the top axis not actually pointing
790  // left, but their corresponding closest edges of a rectangular unrotated label polygon are.
791 
792  int relAngle = axisAngle - labelTA.rotation() + 45;
793  if ( relAngle < 0 ) {
794  relAngle += 360;
795  }
796  int polyCorner1 = relAngle / 90;
797  QPoint p1 = labelPoly.at( polyCorner1 );
798  QPoint p2 = labelPoly.at( polyCorner1 == 3 ? 0 : ( polyCorner1 + 1 ) );
799 
800  QPointF labelPos = tickEnd;
801 
802  qreal labelMargin = rulerAttr.labelMargin();
803  if ( labelMargin < 0 ) {
804  labelMargin = QFontMetricsF( tickLabel->realFont() ).height() * 0.5;
805  }
806  labelMargin -= tickLabel->marginWidth(); // make up for the margin that's already there
807 
808  switch ( position() ) {
809  case Left:
810  labelPos += QPointF( -size.width() - labelMargin,
811  -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) );
812  break;
813  case Right:
814  labelPos += QPointF( labelMargin,
815  -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) );
816  break;
817  case Top:
818  labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ),
819  -size.height() - labelMargin );
820  break;
821  case Bottom:
822  labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ),
823  labelMargin );
824  break;
825  }
826 
827  tickLabel->setGeometry( QRect( labelPos.toPoint(), size.toSize() ) );
828 
829  if ( step == Painting ) {
830  tickLabel->paint( painter );
831  }
832 
833  // collision check the current label against the previous one
834 
835  // like in the old code, we don't shorten or decimate labels if they are already the
836  // manual short type, or if they are the manual long type and on the vertical axis
837  // ### they can still collide though, especially when they're rotated!
838  if ( step == Layout ) {
839  int spaceSavingRotation = geoXy( 270, 0 );
840  bool canRotate = labelTA.autoRotate() && labelTA.rotation() != spaceSavingRotation;
841  const bool canShortenLabels = !geoXy.isY && it.type() == TickIterator::MajorTickManualLong &&
842  it.hasShorterLabels();
843  bool collides = false;
844  if ( it.type() == TickIterator::MajorTick || it.type() == TickIterator::MajorTickHeaderDataLabel
845  || canShortenLabels || canRotate ) {
846  if ( isFirstLabel ) {
847  isFirstLabel = false;
848  } else {
849  collides = tickLabel->intersects( *prevTickLabel, labelPos, prevTickLabelPos );
850  qSwap( prevTickLabel, tickLabel );
851  }
852  prevTickLabelPos = labelPos;
853  }
854  if ( collides ) {
855  // to make room, we try in order: shorten, rotate, decimate
856  if ( canRotate && !canShortenLabels ) {
857  labelTA.setRotation( spaceSavingRotation );
858  // tickLabel will be reused in the next round
859  tickLabel->setTextAttributes( labelTA );
860  } else {
861  labelThinningFactor++;
862  }
863  step--; // relayout
864  break;
865  }
866  }
867  }
868  }
869  delete tickLabel;
870  tickLabel = 0;
871  delete prevTickLabel;
872  prevTickLabel = 0;
873 
874  if ( ! titleText().isEmpty() ) {
875  d->drawTitleText( painter, plane, d->axis()->geometry() );
876  }
877 }
878 
879 /* pure virtual in QLayoutItem */
881 {
882  return false; // if the axis exists, it has some (perhaps default) content
883 }
884 
885 /* pure virtual in QLayoutItem */
886 Qt::Orientations CartesianAxis::expandingDirections() const
887 {
888  Qt::Orientations ret;
889  switch ( position() ) {
890  case Bottom:
891  case Top:
892  ret = Qt::Horizontal;
893  break;
894  case Left:
895  case Right:
896  ret = Qt::Vertical;
897  break;
898  default:
899  Q_ASSERT( false );
900  break;
901  };
902  return ret;
903 }
904 
906 {
907  d->cachedMaximumSize = QSize();
908 }
909 
910 /* pure virtual in QLayoutItem */
912 {
913  if ( ! d->cachedMaximumSize.isValid() )
914  d->cachedMaximumSize = d->calculateMaximumSize();
915  return d->cachedMaximumSize;
916 }
917 
918 QSize CartesianAxis::Private::calculateMaximumSize() const
919 {
920  if ( !diagram() ) {
921  return QSize();
922  }
923 
924  CartesianCoordinatePlane* plane = dynamic_cast< CartesianCoordinatePlane* >( diagram()->coordinatePlane() );
925  Q_ASSERT( plane );
926  QObject* refArea = plane->parent();
927  const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( diagram() )
928  && axis()->isAbscissa();
929 
930  // we ignore:
931  // - label thinning (expensive, not worst case and we want worst case)
932  // - label autorotation (expensive, obscure feature(?))
933  // - axis length (it is determined by the plane / diagram / chart anyway)
934  // - the title's influence on axis length; this one might be TODO. See KDCH-863.
935 
936  XySwitch geoXy( isVertical() );
937  qreal size = 0; // this is the size transverse to the axis direction
938 
939  // the following variables describe how much the first and last label stick out over the axis
940  // area, so that the geometry of surrounding layout items can be adjusted to make room.
941  qreal startOverhang = 0.0;
942  qreal endOverhang = 0.0;
943 
944  if ( mAxis->textAttributes().isVisible() ) {
945  // these four are used just to calculate startOverhang and endOverhang
946  qreal lowestLabelPosition = signalingNaN;
947  qreal highestLabelPosition = signalingNaN;
948  qreal lowestLabelLongitudinalSize = signalingNaN;
949  qreal highestLabelLongitudinalSize = signalingNaN;
950 
951  TextLayoutItem tickLabel( QString(), mAxis->textAttributes(), refArea,
953  const RulerAttributes rulerAttr = mAxis->rulerAttributes();
954 
955  bool showFirstTick = rulerAttr.showFirstTick();
956  for ( TickIterator it( axis(), plane, 1, centerTicks ); !it.isAtEnd(); ++it ) {
957  const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. );
958  if ( !showFirstTick ) {
959  showFirstTick = true;
960  continue;
961  }
962 
963  qreal labelSizeTransverse = 0.0;
964  qreal labelMargin = 0.0;
965  if ( !it.text().isEmpty() ) {
966  QPointF labelPosition = plane->translate( QPointF( geoXy( drawPos, 1.0 ),
967  geoXy( 1.0, drawPos ) ) );
968  highestLabelPosition = geoXy( labelPosition.x(), labelPosition.y() );
969 
970  QString text = it.text();
971  if ( it.type() == TickIterator::MajorTick ) {
972  text = customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() );
973  }
974  tickLabel.setText( text );
975 
976  QSize sz = tickLabel.sizeHint();
977  highestLabelLongitudinalSize = geoXy( sz.width(), sz.height() );
978  if ( ISNAN( lowestLabelLongitudinalSize ) ) {
979  lowestLabelLongitudinalSize = highestLabelLongitudinalSize;
980  lowestLabelPosition = highestLabelPosition;
981  }
982 
983  labelSizeTransverse = geoXy( sz.height(), sz.width() );
984  labelMargin = rulerAttr.labelMargin();
985  if ( labelMargin < 0 ) {
986  labelMargin = QFontMetricsF( tickLabel.realFont() ).height() * 0.5;
987  }
988  labelMargin -= tickLabel.marginWidth(); // make up for the margin that's already there
989  }
990  qreal tickLength = it.type() == TickIterator::CustomTick ?
991  customTickLength : axis()->tickLength( it.type() == TickIterator::MinorTick );
992  size = qMax( size, tickLength + labelMargin + labelSizeTransverse );
993  }
994 
995  const DataDimension dimX = plane->gridDimensionsList().first();
996  const DataDimension dimY = plane->gridDimensionsList().last();
997 
998  QPointF pt = plane->translate( QPointF( dimX.start, dimY.start ) );
999  const qreal lowestPosition = geoXy( pt.x(), pt.y() );
1000  pt = plane->translate( QPointF( dimX.end, dimY.end ) );
1001  const qreal highestPosition = geoXy( pt.x(), pt.y() );
1002 
1003  // the geoXy( 1.0, -1.0 ) here is necessary because Qt's y coordinate is inverted
1004  startOverhang = qMax( 0.0, ( lowestPosition - lowestLabelPosition ) * geoXy( 1.0, -1.0 ) +
1005  lowestLabelLongitudinalSize * 0.5 );
1006  endOverhang = qMax( 0.0, ( highestLabelPosition - highestPosition ) * geoXy( 1.0, -1.0 ) +
1007  highestLabelLongitudinalSize * 0.5 );
1008  }
1009 
1010  amountOfLeftOverlap = geoXy( startOverhang, 0.0 );
1011  amountOfRightOverlap = geoXy( endOverhang, 0.0 );
1012  amountOfBottomOverlap = geoXy( 0.0, startOverhang );
1013  amountOfTopOverlap = geoXy( 0.0, endOverhang );
1014 
1015  const TextAttributes titleTA = titleTextAttributesWithAdjustedRotation();
1016  if ( titleTA.isVisible() && !axis()->titleText().isEmpty() ) {
1017  TextLayoutItem title( axis()->titleText(), titleTA, refArea, KDChartEnums::MeasureOrientationMinimum,
1018  Qt::AlignHCenter | Qt::AlignVCenter );
1019 
1020  QFontMetricsF titleFM( title.realFont(), GlobalMeasureScaling::paintDevice() );
1021  size += geoXy( titleFM.height() * 0.33, titleFM.averageCharWidth() * 0.55 ); // spacing
1022  size += geoXy( title.sizeHint().height(), title.sizeHint().width() );
1023  }
1024 
1025  // the size parallel to the axis direction is not determined by us, so we just return 1
1026  return QSize( geoXy( 1, int( size ) ), geoXy( int ( size ), 1 ) );
1027 }
1028 
1029 /* pure virtual in QLayoutItem */
1031 {
1032  return maximumSize();
1033 }
1034 
1035 /* pure virtual in QLayoutItem */
1037 {
1038  return maximumSize();
1039 }
1040 
1041 /* pure virtual in QLayoutItem */
1042 void CartesianAxis::setGeometry( const QRect& r )
1043 {
1044  if ( d->geometry != r ) {
1045  d->geometry = r;
1047  }
1048 }
1049 
1050 /* pure virtual in QLayoutItem */
1052 {
1053  return d->geometry;
1054 }
1055 
1057 {
1058  d->customTickLength = value;
1059 }
1060 
1062 {
1063  return d->customTickLength;
1064 }
1065 
1066 int CartesianAxis::tickLength( bool subUnitTicks ) const
1067 {
1068  const RulerAttributes& rulerAttr = rulerAttributes();
1069  return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength();
1070 }
1071 
1073 {
1074  return d->annotations;
1075 }
1076 
1078 {
1079  if ( d->annotations == annotations )
1080  return;
1081 
1082  d->annotations = annotations;
1083  update();
1084 }
1085 
1087 {
1088  return d->customTicksPositions;
1089 }
1090 
1091 void CartesianAxis::setCustomTicks( const QList< qreal >& customTicksPositions )
1092 {
1093  if ( d->customTicksPositions == customTicksPositions )
1094  return;
1095 
1096  d->customTicksPositions = customTicksPositions;
1097  update();
1098 }

Klarälvdalens Datakonsult AB (KDAB)
Qt-related services and products
http://www.kdab.com/
http://www.kdab.com/products/kd-chart/