Version 6.1.4.1, tag libreoffice-6.1.4.1
[LibreOffice.git] / chart2 / source / view / charttypes / PieChart.cxx
blob3cc4dc3817ea08bd7d1e04e6f344bf2ad8652232
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include "PieChart.hxx"
21 #include <PlottingPositionHelper.hxx>
22 #include <AbstractShapeFactory.hxx>
23 #include <PolarLabelPositionHelper.hxx>
24 #include <CommonConverters.hxx>
25 #include <ViewDefines.hxx>
26 #include <ObjectIdentifier.hxx>
28 #include <com/sun/star/chart/DataLabelPlacement.hpp>
29 #include <com/sun/star/chart2/XColorScheme.hpp>
31 #include <com/sun/star/container/XChild.hpp>
32 #include <rtl/math.hxx>
34 #include <memory>
36 using namespace ::com::sun::star;
37 using namespace ::com::sun::star::chart2;
39 namespace chart {
41 struct PieChart::ShapeParam
43 /** the start angle of the slice
45 double mfUnitCircleStartAngleDegree;
47 /** the angle width of the slice
49 double mfUnitCircleWidthAngleDegree;
51 /** the normalized outer radius of the ring the slice belongs to.
53 double mfUnitCircleOuterRadius;
55 /** the normalized inner radius of the ring the slice belongs to
57 double mfUnitCircleInnerRadius;
59 /** relative distance offset of a slice from the pie center;
60 * this parameter is used for instance when the user performs manual
61 * dragging of a slice (the drag operation is possible only for slices that
62 * belong to the outer ring and only along the ray bisecting the slice);
63 * the value for the given entry in the data series is obtained by the
64 * `Offset` property attached to each entry; note that the value
65 * provided by the `Offset` property is used both as a logical value in
66 * `PiePositionHelper::getInnerAndOuterRadius` and as a percentage value in
67 * the `PieChart::createDataPoint` and `PieChart::createTextLabelShape`
68 * methods; since the logical height of a ring is always 1, this duality
69 * does not cause any incorrect behavior;
71 double mfExplodePercentage;
73 /** sum of all Y values in a single series
75 double mfLogicYSum;
77 /** for 3D pie chart: label z coordinate
79 double mfLogicZ;
81 /** for 3D pie chart: height
83 double mfDepth;
85 ShapeParam() :
86 mfUnitCircleStartAngleDegree(0.0),
87 mfUnitCircleWidthAngleDegree(0.0),
88 mfUnitCircleOuterRadius(0.0),
89 mfUnitCircleInnerRadius(0.0),
90 mfExplodePercentage(0.0),
91 mfLogicYSum(0.0),
92 mfLogicZ(0.0),
93 mfDepth(0.0) {}
96 class PiePositionHelper : public PolarPlottingPositionHelper
98 public:
99 PiePositionHelper( double fAngleDegreeOffset );
101 bool getInnerAndOuterRadius( double fCategoryX, double& fLogicInnerRadius, double& fLogicOuterRadius, bool bUseRings, double fMaxOffset ) const;
103 public:
104 //Distance between different category rings, seen relative to width of a ring:
105 double m_fRingDistance; //>=0 m_fRingDistance=1 --> distance == width
108 PiePositionHelper::PiePositionHelper( double fAngleDegreeOffset )
109 : m_fRingDistance(0.0)
111 m_fRadiusOffset = 0.0;
112 m_fAngleDegreeOffset = fAngleDegreeOffset;
115 /** Compute the outer and the inner radius for the current ring (not for the
116 * whole donut!), in general it is:
117 * inner_radius = (ring_index + 1) - 0.5 + max_offset,
118 * outer_radius = (ring_index + 1) + 0.5 + max_offset.
119 * When orientation for the radius axis is reversed these values are swapped.
120 * (Indeed the orientation for the radius axis is always reversed!
121 * See `PieChartTypeTemplate::adaptScales`.)
122 * The maximum relative offset (see notes for `PieChart::getMaxOffset`) is
123 * added to both the inner and the outer radius.
124 * It returns true if the ring is visible (that is not out of the radius
125 * axis scale range).
127 bool PiePositionHelper::getInnerAndOuterRadius( double fCategoryX
128 , double& fLogicInnerRadius, double& fLogicOuterRadius
129 , bool bUseRings, double fMaxOffset ) const
131 if( !bUseRings )
132 fCategoryX = 1.0;
134 double fLogicInner = fCategoryX -0.5+m_fRingDistance/2.0;
135 double fLogicOuter = fCategoryX +0.5-m_fRingDistance/2.0;
137 if( !isMathematicalOrientationRadius() )
139 //in this case the given getMaximumX() was not correct instead the minimum should have been smaller by fMaxOffset
140 //but during getMaximumX and getMimumX we do not know the axis orientation
141 fLogicInner += fMaxOffset;
142 fLogicOuter += fMaxOffset;
145 if( fLogicInner >= getLogicMaxX() )
146 return false;
147 if( fLogicOuter <= getLogicMinX() )
148 return false;
150 if( fLogicInner < getLogicMinX() )
151 fLogicInner = getLogicMinX();
152 if( fLogicOuter > getLogicMaxX() )
153 fLogicOuter = getLogicMaxX();
155 fLogicInnerRadius = fLogicInner;
156 fLogicOuterRadius = fLogicOuter;
157 if( !isMathematicalOrientationRadius() )
158 std::swap(fLogicInnerRadius,fLogicOuterRadius);
159 return true;
162 PieChart::PieChart( const uno::Reference<XChartType>& xChartTypeModel
163 , sal_Int32 nDimensionCount
164 , bool bExcludingPositioning )
165 : VSeriesPlotter( xChartTypeModel, nDimensionCount )
166 , m_pPosHelper( new PiePositionHelper( (m_nDimension==3) ? 0.0 : 90.0 ) )
167 , m_bUseRings(false)
168 , m_bSizeExcludesLabelsAndExplodedSegments(bExcludingPositioning)
170 ::rtl::math::setNan(&m_fMaxOffset);
172 PlotterBase::m_pPosHelper = m_pPosHelper.get();
173 VSeriesPlotter::m_pMainPosHelper = m_pPosHelper.get();
174 m_pPosHelper->m_fRadiusOffset = 0.0;
175 m_pPosHelper->m_fRingDistance = 0.0;
177 uno::Reference< beans::XPropertySet > xChartTypeProps( xChartTypeModel, uno::UNO_QUERY );
178 if( xChartTypeProps.is() ) try
180 xChartTypeProps->getPropertyValue( "UseRings") >>= m_bUseRings;
181 if( m_bUseRings )
183 m_pPosHelper->m_fRadiusOffset = 1.0;
184 if( nDimensionCount==3 )
185 m_pPosHelper->m_fRingDistance = 0.1;
188 catch( const uno::Exception& e )
190 SAL_WARN("chart2", "Exception caught. " << e );
194 PieChart::~PieChart()
198 void PieChart::setScales( const std::vector< ExplicitScaleData >& rScales, bool /* bSwapXAndYAxis */ )
200 OSL_ENSURE(m_nDimension<=static_cast<sal_Int32>(rScales.size()),"Dimension of Plotter does not fit two dimension of given scale sequence");
201 m_pPosHelper->setScales( rScales, true );
204 drawing::Direction3D PieChart::getPreferredDiagramAspectRatio() const
206 if( m_nDimension == 3 )
207 return drawing::Direction3D(1,1,0.10);
208 return drawing::Direction3D(1,1,1);
211 bool PieChart::shouldSnapRectToUsedArea()
213 return true;
216 uno::Reference< drawing::XShape > PieChart::createDataPoint(
217 const uno::Reference<drawing::XShapes>& xTarget,
218 const uno::Reference<beans::XPropertySet>& xObjectProperties,
219 tPropertyNameValueMap const * pOverwritePropertiesMap,
220 const ShapeParam& rParam )
222 //transform position:
223 drawing::Direction3D aOffset;
224 if (rParam.mfExplodePercentage != 0.0)
226 double fAngle = rParam.mfUnitCircleStartAngleDegree + rParam.mfUnitCircleWidthAngleDegree/2.0;
227 double fRadius = (rParam.mfUnitCircleOuterRadius-rParam.mfUnitCircleInnerRadius)*rParam.mfExplodePercentage;
228 drawing::Position3D aOrigin = m_pPosHelper->transformUnitCircleToScene(0, 0, rParam.mfLogicZ);
229 drawing::Position3D aNewOrigin = m_pPosHelper->transformUnitCircleToScene(fAngle, fRadius, rParam.mfLogicZ);
230 aOffset = aNewOrigin - aOrigin;
233 //create point
234 uno::Reference< drawing::XShape > xShape(nullptr);
235 if(m_nDimension==3)
237 xShape = m_pShapeFactory->createPieSegment( xTarget
238 , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree
239 , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius
240 , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() )
241 , rParam.mfDepth );
243 else
245 xShape = m_pShapeFactory->createPieSegment2D( xTarget
246 , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree
247 , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius
248 , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() ) );
250 setMappedProperties( xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties(), pOverwritePropertiesMap );
251 return xShape;
254 void PieChart::createTextLabelShape(
255 const uno::Reference<drawing::XShapes>& xTextTarget,
256 VDataSeries& rSeries, sal_Int32 nPointIndex, ShapeParam& rParam )
258 if (!rSeries.getDataPointLabelIfLabel(nPointIndex))
259 // There is no text label for this data point. Nothing to do.
260 return;
262 ///by using the `mfExplodePercentage` parameter a normalized offset is added
263 ///to both normalized radii. (See notes for
264 ///`PolarPlottingPositionHelper::transformToRadius`, especially example 3,
265 ///and related comments).
266 if (rParam.mfExplodePercentage != 0.0)
268 double fExplodeOffset = (rParam.mfUnitCircleOuterRadius-rParam.mfUnitCircleInnerRadius)*rParam.mfExplodePercentage;
269 rParam.mfUnitCircleInnerRadius += fExplodeOffset;
270 rParam.mfUnitCircleOuterRadius += fExplodeOffset;
273 ///get the required label placement type. Available placements are
274 ///`AVOID_OVERLAP`, `CENTER`, `OUTSIDE` and `INSIDE`;
275 sal_Int32 nLabelPlacement = rSeries.getLabelPlacement(
276 nPointIndex, m_xChartTypeModel, m_pPosHelper->isSwapXAndY());
278 ///when the placement is of `AVOID_OVERLAP` type a later rearrangement of
279 ///the label position is allowed; the `createTextLabelShape` treats the
280 ///`AVOID_OVERLAP` as if it was of `CENTER` type;
282 double nVal = rSeries.getYValue(nPointIndex);
283 //AVOID_OVERLAP is in fact "Best fit" in the UI.
284 bool bMovementAllowed = ( nLabelPlacement == css::chart::DataLabelPlacement::AVOID_OVERLAP );
285 if( bMovementAllowed )
287 // Use center for "Best fit" for now. In the future we
288 // may want to implement a real best fit algorithm.
289 // But center is good enough, and close to what Excel
290 // does.
292 // Place the label outside if the sector is too small
293 // The threshold is set to 2%, but can be improved by making it a function of
294 // label width and radius too ?
295 double fFrac = fabs( nVal / rParam.mfLogicYSum );
296 nLabelPlacement = ( fFrac <= 0.02 ) ? css::chart::DataLabelPlacement::OUTSIDE :
297 css::chart::DataLabelPlacement::CENTER;
300 ///for `OUTSIDE` (`INSIDE`) label placements an offset of 150 (-150), in the
301 ///radius direction, is added to the final screen position of the label
302 ///anchor point. This is required in order to ensure that the label is
303 ///completely outside (inside) the related slice. Indeed this value should
304 ///depend on the font height;
305 ///pay attention: 150 is not a big offset, in fact the screen position
306 ///coordinates for label anchor points are in the 10000-20000 range, hence
307 ///these are coordinates of a virtual screen and 150 is a small value;
308 LabelAlignment eAlignment(LABEL_ALIGN_CENTER);
309 sal_Int32 nScreenValueOffsetInRadiusDirection = 0 ;
310 if( nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE )
311 nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? 150 : 0;//todo maybe calculate this font height dependent
312 else if( nLabelPlacement == css::chart::DataLabelPlacement::INSIDE )
313 nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? -150 : 0;//todo maybe calculate this font height dependent
315 ///the scene position of the label anchor point is calculated (see notes for
316 ///`PolarLabelPositionHelper::getLabelScreenPositionAndAlignmentForUnitCircleValues`),
317 ///and immediately transformed into the screen position.
318 PolarLabelPositionHelper aPolarPosHelper(m_pPosHelper.get(),m_nDimension,m_xLogicTarget,m_pShapeFactory);
319 awt::Point aScreenPosition2D(
320 aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(eAlignment, nLabelPlacement
321 , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree
322 , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius, rParam.mfLogicZ+0.5, 0 ));
324 ///the screen position of the pie/donut center is calculated.
325 PieLabelInfo aPieLabelInfo;
326 aPieLabelInfo.aFirstPosition = basegfx::B2IVector( aScreenPosition2D.X, aScreenPosition2D.Y );
327 awt::Point aOrigin( aPolarPosHelper.transformSceneToScreenPosition( m_pPosHelper->transformUnitCircleToScene( 0.0, 0.0, rParam.mfLogicZ+1.0 ) ) );
328 aPieLabelInfo.aOrigin = basegfx::B2IVector( aOrigin.X, aOrigin.Y );
330 ///add a scaling independent Offset if requested
331 if( nScreenValueOffsetInRadiusDirection != 0)
333 basegfx::B2IVector aDirection( aScreenPosition2D.X- aOrigin.X, aScreenPosition2D.Y- aOrigin.Y );
334 aDirection.setLength(nScreenValueOffsetInRadiusDirection);
335 aScreenPosition2D.X += aDirection.getX();
336 aScreenPosition2D.Y += aDirection.getY();
339 // compute outer pie radius
340 awt::Point aOuterCirclePoint = PlottingPositionHelper::transformSceneToScreenPosition(
341 m_pPosHelper->transformUnitCircleToScene(
343 rParam.mfUnitCircleOuterRadius,
344 0 ),
345 m_xLogicTarget, m_pShapeFactory, m_nDimension );
346 basegfx::B2IVector aRadiusVector(
347 aOuterCirclePoint.X - aPieLabelInfo.aOrigin.getX(),
348 aOuterCirclePoint.Y - aPieLabelInfo.aOrigin.getY() );
349 double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector);
350 double fPieRadius = sqrt( fSquaredPieRadius );
352 // set the maximum text width to be used when text wrapping is enabled
353 double fTextMaximumFrameWidth = 0.8 * fPieRadius;
354 sal_Int32 nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth);
356 ///the text shape for the label is created
357 aPieLabelInfo.xTextShape = createDataLabel(
358 xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum,
359 aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth);
361 ///a new `PieLabelInfo` instance is initialized with all the info related to
362 ///the current label in order to simplify later label position rearrangement;
363 uno::Reference< container::XChild > xChild( aPieLabelInfo.xTextShape, uno::UNO_QUERY );
365 ///text shape could be empty; in that case there is no need to add label info
366 if( !xChild.is() )
367 return;
369 aPieLabelInfo.xLabelGroupShape.set( xChild->getParent(), uno::UNO_QUERY );
371 aPieLabelInfo.fValue = nVal;
372 aPieLabelInfo.bMovementAllowed = bMovementAllowed;
373 aPieLabelInfo.bMoved= false;
374 aPieLabelInfo.xTextTarget = xTextTarget;
376 if (bMovementAllowed)
378 performLabelBestFit(rParam, aPieLabelInfo);
381 m_aLabelInfoList.push_back(aPieLabelInfo);
384 void PieChart::addSeries( VDataSeries* pSeries, sal_Int32 /* zSlot */, sal_Int32 /* xSlot */, sal_Int32 /* ySlot */ )
386 VSeriesPlotter::addSeries( pSeries, 0, -1, 0 );
389 double PieChart::getMinimumX()
391 return 0.5;
393 double PieChart::getMaxOffset()
395 if (!::rtl::math::isNan(m_fMaxOffset))
396 // Value already cached. Use it.
397 return m_fMaxOffset;
399 m_fMaxOffset = 0.0;
400 if( m_aZSlots.empty() )
401 return m_fMaxOffset;
402 if( m_aZSlots.front().empty() )
403 return m_fMaxOffset;
405 const std::vector< VDataSeries* >& rSeriesList( m_aZSlots.front().front().m_aSeriesVector );
406 if(rSeriesList.empty())
407 return m_fMaxOffset;
409 VDataSeries* pSeries = rSeriesList.front();
410 uno::Reference< beans::XPropertySet > xSeriesProp( pSeries->getPropertiesOfSeries() );
411 if( !xSeriesProp.is() )
412 return m_fMaxOffset;
414 double fExplodePercentage=0.0;
415 xSeriesProp->getPropertyValue( "Offset") >>= fExplodePercentage;
416 if(fExplodePercentage>m_fMaxOffset)
417 m_fMaxOffset=fExplodePercentage;
419 if(!m_bSizeExcludesLabelsAndExplodedSegments)
421 uno::Sequence< sal_Int32 > aAttributedDataPointIndexList;
422 if( xSeriesProp->getPropertyValue( "AttributedDataPoints" ) >>= aAttributedDataPointIndexList )
424 for(sal_Int32 nN=aAttributedDataPointIndexList.getLength();nN--;)
426 uno::Reference< beans::XPropertySet > xPointProp( pSeries->getPropertiesOfPoint(aAttributedDataPointIndexList[nN]) );
427 if(xPointProp.is())
429 fExplodePercentage=0.0;
430 xPointProp->getPropertyValue( "Offset") >>= fExplodePercentage;
431 if(fExplodePercentage>m_fMaxOffset)
432 m_fMaxOffset=fExplodePercentage;
437 return m_fMaxOffset;
439 double PieChart::getMaximumX()
441 double fMaxOffset = getMaxOffset();
442 if( !m_aZSlots.empty() && m_bUseRings)
443 return m_aZSlots.front().size()+0.5+fMaxOffset;
444 return 1.5+fMaxOffset;
446 double PieChart::getMinimumYInRange( double /* fMinimumX */, double /* fMaximumX */, sal_Int32 /* nAxisIndex */ )
448 return 0.0;
451 double PieChart::getMaximumYInRange( double /* fMinimumX */, double /* fMaximumX */, sal_Int32 /* nAxisIndex */ )
453 return 1.0;
456 bool PieChart::isExpandBorderToIncrementRhythm( sal_Int32 /* nDimensionIndex */ )
458 return false;
461 bool PieChart::isExpandIfValuesCloseToBorder( sal_Int32 /* nDimensionIndex */ )
463 return false;
466 bool PieChart::isExpandWideValuesToZero( sal_Int32 /* nDimensionIndex */ )
468 return false;
471 bool PieChart::isExpandNarrowValuesTowardZero( sal_Int32 /* nDimensionIndex */ )
473 return false;
476 bool PieChart::isSeparateStackingForDifferentSigns( sal_Int32 /* nDimensionIndex */ )
478 return false;
481 void PieChart::createShapes()
483 ///a ZSlot is a vector< vector< VDataSeriesGroup > >. There is only one
484 ///ZSlot: m_aZSlots[0] which has a number of elements equal to the total
485 ///number of data series (in fact, even if m_aZSlots[0][i] is an object of
486 ///type `VDataSeriesGroup`, in the current implementation, there is only one
487 ///data series in each data series group).
488 if (m_aZSlots.empty())
489 // No series to plot.
490 return;
492 ///m_xLogicTarget is where the group of all data series shapes (e.g. a pie
493 ///slice) is added (xSeriesTarget);
495 ///m_xFinalTarget is where the group of all text shapes (labels) is added
496 ///(xTextTarget).
498 ///both have been already created and added to the same root shape
499 ///( a member of a VDiagram object); this initialization occurs in
500 ///`ChartView::impl_createDiagramAndContent`.
502 OSL_ENSURE(m_pShapeFactory && m_xLogicTarget.is() && m_xFinalTarget.is(), "PieChart is not properly initialized.");
503 if (!m_pShapeFactory || !m_xLogicTarget.is() || !m_xFinalTarget.is())
504 return;
506 ///the text labels should be always on top of the other series shapes
507 ///therefore create an own group for the texts to move them to front
508 ///(because the text group is created after the series group the texts are
509 ///displayed on top)
510 uno::Reference< drawing::XShapes > xSeriesTarget(
511 createGroupShape( m_xLogicTarget ));
512 uno::Reference< drawing::XShapes > xTextTarget(
513 m_pShapeFactory->createGroup2D( m_xFinalTarget ));
514 //check necessary here that different Y axis can not be stacked in the same group? ... hm?
516 ///pay attention that the `m_bSwapXAndY` parameter used by the polar
517 ///plotting position helper is always set to true for pie/donut charts
518 ///(see PieChart::setScales). This fact causes that `createShapes` expects
519 ///that the radius axis scale is the one with index 0 and the angle axis
520 ///scale is the one with index 1.
522 std::vector< VDataSeriesGroup >::iterator aXSlotIter = m_aZSlots.front().begin();
523 const std::vector< VDataSeriesGroup >::const_iterator aXSlotEnd = m_aZSlots.front().end();
525 ///m_bUseRings == true if chart type is `donut`, == false if chart type is
526 ///`pie`; if the chart is of `donut` type we have as many rings as many data
527 ///series, else we have a single ring (a pie) representing the first data
528 ///series;
529 ///for what I can see the radius axis orientation is always reversed and
530 ///the angle axis orientation is always non-reversed;
531 ///the radius axis scale range is [0.5, number of rings + 0.5 + max_offset],
532 ///the angle axis scale range is [0, 1]. The max_offset parameter is used
533 ///for exploded pie chart and its value is 0.5.
535 ///the `explodeable` ring is the first one except when the radius axis
536 ///orientation is reversed (always!?) and we are dealing with a donut: in
537 ///such a case the `explodeable` ring is the last one.
538 std::vector< VDataSeriesGroup >::size_type nExplodeableSlot = 0;
539 if( m_pPosHelper->isMathematicalOrientationRadius() && m_bUseRings )
540 nExplodeableSlot = m_aZSlots.front().size()-1;
542 m_aLabelInfoList.clear();
543 ::rtl::math::setNan(&m_fMaxOffset);
544 sal_Int32 n3DRelativeHeight = 100;
545 uno::Reference< beans::XPropertySet > xPropertySet( m_xChartTypeModel, uno::UNO_QUERY );
546 if ( (m_nDimension==3) && xPropertySet.is())
550 uno::Any aAny = xPropertySet->getPropertyValue( "3DRelativeHeight" );
551 aAny >>= n3DRelativeHeight;
553 catch (const uno::Exception&) { }
555 ///iterate over each xslot, that is on each data series (there is
556 ///only one data series in each data series group!); note that if the chart
557 ///type is a pie the loop iterates only over the first data series
558 ///(m_bUseRings||fSlotX<0.5)
559 for( double fSlotX=0; aXSlotIter != aXSlotEnd && (m_bUseRings||fSlotX<0.5 ); ++aXSlotIter, fSlotX+=1.0 )
561 ShapeParam aParam;
563 std::vector< VDataSeries* >* pSeriesList = &(aXSlotIter->m_aSeriesVector);
564 if(pSeriesList->empty())//there should be only one series in each x slot
565 continue;
566 VDataSeries* pSeries = pSeriesList->front();
567 if(!pSeries)
568 continue;
570 bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor");
572 /// The angle degree offset is set by the same property of the
573 /// data series.
574 /// Counter-clockwise offset from the 3 o'clock position.
575 m_pPosHelper->m_fAngleDegreeOffset = pSeries->getStartingAngle();
577 ///iterate through all points to get the sum of all entries of
578 ///the current data series
579 sal_Int32 nPointIndex=0;
580 sal_Int32 nPointCount=pSeries->getTotalPointCount();
581 for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ )
583 double fY = pSeries->getYValue( nPointIndex );
584 if(fY<0.0)
586 //@todo warn somehow that negative values are treated as positive
588 if( ::rtl::math::isNan(fY) )
589 continue;
590 aParam.mfLogicYSum += fabs(fY);
593 if (aParam.mfLogicYSum == 0.0)
594 // Total sum of all Y values in this series is zero. Skip the whole series.
595 continue;
597 double fLogicYForNextPoint = 0.0;
598 ///iterate through all points to create shapes
599 for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ )
601 double fLogicInnerRadius, fLogicOuterRadius;
603 ///compute the maximum relative distance offset of the current slice
604 ///from the pie center
605 ///it is worth noting that after the first invocation the maximum
606 ///offset value is cached, so it is evaluated only once per each
607 ///call to `createShapes`
608 double fOffset = getMaxOffset();
610 ///compute the outer and the inner radius for the current ring slice
611 bool bIsVisible = m_pPosHelper->getInnerAndOuterRadius( fSlotX+1.0, fLogicInnerRadius, fLogicOuterRadius, m_bUseRings, fOffset );
612 if( !bIsVisible )
613 continue;
615 aParam.mfDepth = getTransformedDepth() * (n3DRelativeHeight / 100.0);
617 uno::Reference< drawing::XShapes > xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries, xSeriesTarget);
618 ///collect data point information (logic coordinates, style ):
619 double fLogicYValue = fabs(pSeries->getYValue( nPointIndex ));
620 if( ::rtl::math::isNan(fLogicYValue) )
621 continue;
622 if(fLogicYValue==0.0)//@todo: continue also if the resolution to small
623 continue;
624 double fLogicYPos = fLogicYForNextPoint;
625 fLogicYForNextPoint += fLogicYValue;
627 uno::Reference< beans::XPropertySet > xPointProperties = pSeries->getPropertiesOfPoint( nPointIndex );
629 //iterate through all subsystems to create partial points
631 //logic values on angle axis:
632 double fLogicStartAngleValue = fLogicYPos / aParam.mfLogicYSum;
633 double fLogicEndAngleValue = (fLogicYPos+fLogicYValue) / aParam.mfLogicYSum;
635 ///note that the explode percentage is set to the `Offset`
636 ///property of the current data series entry only for slices
637 ///belonging to the outer ring
638 aParam.mfExplodePercentage = 0.0;
639 bool bDoExplode = ( nExplodeableSlot == static_cast< std::vector< VDataSeriesGroup >::size_type >(fSlotX) );
640 if(bDoExplode) try
642 xPointProperties->getPropertyValue( "Offset") >>= aParam.mfExplodePercentage;
644 catch( const uno::Exception& e )
646 SAL_WARN("chart2", "Exception caught. " << e );
649 ///see notes for `PolarPlottingPositionHelper` methods
650 ///transform to unit circle:
651 aParam.mfUnitCircleWidthAngleDegree = m_pPosHelper->getWidthAngleDegree( fLogicStartAngleValue, fLogicEndAngleValue );
652 aParam.mfUnitCircleStartAngleDegree = m_pPosHelper->transformToAngleDegree( fLogicStartAngleValue );
653 aParam.mfUnitCircleInnerRadius = m_pPosHelper->transformToRadius( fLogicInnerRadius );
654 aParam.mfUnitCircleOuterRadius = m_pPosHelper->transformToRadius( fLogicOuterRadius );
656 ///point color:
657 std::unique_ptr< tPropertyNameValueMap > apOverwritePropertiesMap(nullptr);
658 if (!pSeries->hasPointOwnColor(nPointIndex) && m_xColorScheme.is())
660 apOverwritePropertiesMap.reset( new tPropertyNameValueMap );
661 (*apOverwritePropertiesMap)["FillColor"] <<=
662 m_xColorScheme->getColorByIndex( nPointIndex );
665 ///create data point
666 aParam.mfLogicZ = -1.0; // For 3D pie chart label position
667 uno::Reference<drawing::XShape> xPointShape =
668 createDataPoint(
669 xSeriesGroupShape_Shapes, xPointProperties, apOverwritePropertiesMap.get(), aParam);
671 if(bHasFillColorMapping)
673 double nPropVal = pSeries->getValueByProperty(nPointIndex, "FillColor");
674 if(!rtl::math::isNan(nPropVal))
676 uno::Reference< beans::XPropertySet > xProps( xPointShape, uno::UNO_QUERY_THROW );
677 xProps->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>( nPropVal)));
681 ///create label
682 createTextLabelShape(xTextTarget, *pSeries, nPointIndex, aParam);
684 if(!bDoExplode)
686 AbstractShapeFactory::setShapeName( xPointShape
687 , ObjectIdentifier::createPointCID( pSeries->getPointCID_Stub(), nPointIndex ) );
689 else try
691 ///enable dragging of outer segments
693 double fAngle = aParam.mfUnitCircleStartAngleDegree + aParam.mfUnitCircleWidthAngleDegree/2.0;
694 double fMaxDeltaRadius = aParam.mfUnitCircleOuterRadius-aParam.mfUnitCircleInnerRadius;
695 drawing::Position3D aOrigin = m_pPosHelper->transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius, aParam.mfLogicZ );
696 drawing::Position3D aNewOrigin = m_pPosHelper->transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius + fMaxDeltaRadius, aParam.mfLogicZ );
698 sal_Int32 nOffsetPercent( static_cast<sal_Int32>(aParam.mfExplodePercentage * 100.0) );
700 awt::Point aMinimumPosition( PlottingPositionHelper::transformSceneToScreenPosition(
701 aOrigin, m_xLogicTarget, m_pShapeFactory, m_nDimension ) );
702 awt::Point aMaximumPosition( PlottingPositionHelper::transformSceneToScreenPosition(
703 aNewOrigin, m_xLogicTarget, m_pShapeFactory, m_nDimension ) );
705 //enable dragging of piesegments
706 OUString aPointCIDStub( ObjectIdentifier::createSeriesSubObjectStub( OBJECTTYPE_DATA_POINT
707 , pSeries->getSeriesParticle()
708 , ObjectIdentifier::getPieSegmentDragMethodServiceName()
709 , ObjectIdentifier::createPieSegmentDragParameterString(
710 nOffsetPercent, aMinimumPosition, aMaximumPosition )
711 ) );
713 AbstractShapeFactory::setShapeName( xPointShape
714 , ObjectIdentifier::createPointCID( aPointCIDStub, nPointIndex ) );
716 catch( const uno::Exception& e )
718 SAL_WARN("chart2", "Exception caught. " << e );
720 }//next series in x slot (next y slot)
721 }//next category
722 }//next x slot
725 namespace
728 ::basegfx::B2IRectangle lcl_getRect( const uno::Reference< drawing::XShape >& xShape )
730 ::basegfx::B2IRectangle aRect;
731 if( xShape.is() )
732 aRect = BaseGFXHelper::makeRectangle(xShape->getPosition(),xShape->getSize() );
733 return aRect;
736 bool lcl_isInsidePage( const awt::Point& rPos, const awt::Size& rSize, const awt::Size& rPageSize )
738 if( rPos.X < 0 || rPos.Y < 0 )
739 return false;
740 if( (rPos.X + rSize.Width) > rPageSize.Width )
741 return false;
742 if( (rPos.Y + rSize.Height) > rPageSize.Height )
743 return false;
744 return true;
747 inline
748 double lcl_radToDeg(double fAngleRad)
750 return (fAngleRad / M_PI) * 180.0;
753 inline
754 double lcl_degToRad(double fAngleDeg)
756 return (fAngleDeg / 180) * M_PI;
759 inline
760 double lcl_getDegAngleInStandardRange(double fAngle)
762 while( fAngle < 0.0 )
763 fAngle += 360.0;
764 while( fAngle >= 360.0 )
765 fAngle -= 360.0;
766 return fAngle;
769 }//end anonymous namespace
771 PieChart::PieLabelInfo::PieLabelInfo()
772 : xTextShape(nullptr), xLabelGroupShape(nullptr), aFirstPosition(), aOrigin(), fValue(0.0)
773 , bMovementAllowed(false), bMoved(false), xTextTarget(nullptr), pPrevious(nullptr),pNext(nullptr)
777 /** In case this label and the passed label overlap the routine moves this
778 * label in order to fix the issue. After the label position has been
779 * rearranged it is checked that the moved label is still inside the page
780 * document, if the test is positive the routine returns true else returns
781 * false.
783 bool PieChart::PieLabelInfo::moveAwayFrom( const PieChart::PieLabelInfo* pFix, const awt::Size& rPageSize, bool bMoveHalfWay, bool bMoveClockwise )
785 //return true if the move was successful
786 if(!bMovementAllowed)
787 return false;
789 const sal_Int32 nLabelDistanceX = rPageSize.Width/50;
790 const sal_Int32 nLabelDistanceY = rPageSize.Height/50;
792 ///compute the rectangle representing the intersection of the label bounding
793 ///boxes (`aOverlap`).
794 ::basegfx::B2IRectangle aOverlap( lcl_getRect( xLabelGroupShape ) );
795 aOverlap.intersect( lcl_getRect( pFix->xLabelGroupShape ) );
796 if( !aOverlap.isEmpty() )
798 //TODO: alternative move direction
800 ///the label is shifted along the direction orthogonal to the vector
801 ///starting at the pie/donut center and ending at this label anchor
802 ///point;
804 ///named `aTangentialDirection` the unit vector related to such a
805 ///direction, the magnitude of the shift along such a direction is
806 ///calculated in this way: if the horizontal component of
807 ///`aTangentialDirection` is greater than the vertical component,
808 ///the magnitude of the shift is equal to `aOverlap.Width` else to
809 ///`aOverlap.Height`;
810 basegfx::B2IVector aRadiusDirection = aFirstPosition - aOrigin;
811 aRadiusDirection.setLength(1.0);
812 basegfx::B2IVector aTangentialDirection( -aRadiusDirection.getY(), aRadiusDirection.getX() );
813 bool bShiftHorizontal = abs(aTangentialDirection.getX()) > abs(aTangentialDirection.getY());
814 sal_Int32 nShift = bShiftHorizontal ? static_cast<sal_Int32>(aOverlap.getWidth()) : static_cast<sal_Int32>(aOverlap.getHeight());
815 ///the magnitude of the shift is also increased by 1/50-th of the width
816 ///or the height of the document page;
817 nShift += (bShiftHorizontal ? nLabelDistanceX : nLabelDistanceY);
818 ///in case the `bMoveHalfWay` parameter is true the magnitude of
819 ///the shift is halved.
820 if( bMoveHalfWay )
821 nShift/=2;
822 ///in case the `bMoveClockwise` parameter is false the direction of
823 ///`aTangentialDirection` is reversed;
824 if(!bMoveClockwise)
825 nShift*=-1;
826 awt::Point aOldPos( xLabelGroupShape->getPosition() );
827 basegfx::B2IVector aNewPos = basegfx::B2IVector( aOldPos.X, aOldPos.Y ) + nShift*aTangentialDirection;
829 ///a final check is performed in order to be sure that the moved label
830 ///is still inside the page document;
831 awt::Point aNewAWTPos( aNewPos.getX(), aNewPos.getY() );
832 if( !lcl_isInsidePage( aNewAWTPos, xLabelGroupShape->getSize(), rPageSize ) )
833 return false;
835 xLabelGroupShape->setPosition( aNewAWTPos );
836 bMoved = true;
838 return true;
840 ///note that no further test is performed in order to check that the
841 ///overlap is really fixed: this result is surely achieved if the shift
842 ///would occur in the horizontal or vertical direction (since, in such a
843 ///direction, the magnitude of the shift would be greater than the length
844 ///of the overlap), but in general this is not true;
845 ///adding a constant term equal to 1/50-th of the width or the height of
846 ///the document page increases the probability of success, anyway it is
847 ///worth noting that the method can return true even if the overlap issue
848 ///is not (completely) fixed;
851 void PieChart::resetLabelPositionsToPreviousState()
853 for (auto const& labelInfo : m_aLabelInfoList)
854 labelInfo.xLabelGroupShape->setPosition(labelInfo.aPreviousPosition);
857 bool PieChart::detectLabelOverlapsAndMove( const awt::Size& rPageSize )
859 ///the routine tries to individuate a chain of overlapping labels and
860 ///assigns the first and the last of them to `pFirstBorder` and
861 ///`pSecondBorder`;
862 ///this result is achieved by performing two consecutive while loop.
864 ///find borders of a group of overlapping labels
866 ///a first while loop is started on the collection of `PieLabelInfo` objects;
867 ///the bounding box of each label is checked for overlap against the bounding
868 ///box of the previous and of the next label;
869 ///when an overlap is found `bOverlapFound` is set to true, however the
870 ///iteration is break only if the overlap occurs against only the next label
871 ///and not against the previous label: so we exit from the loop whenever an
872 ///overlap occurs except when the loop initial label overlaps with the
873 ///previous one;
874 bool bOverlapFound = false;
875 PieLabelInfo* pStart = &(*(m_aLabelInfoList.rbegin()));
876 PieLabelInfo* pFirstBorder = nullptr;
877 PieLabelInfo* pSecondBorder = nullptr;
878 PieLabelInfo* pCurrent = pStart;
881 ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) );
882 ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap );
883 aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) );
884 aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) );
886 bool bPreviousOverlap = !aPreviousOverlap.isEmpty();
887 bool bNextOverlap = !aNextOverlap.isEmpty();
888 if( bPreviousOverlap || bNextOverlap )
889 bOverlapFound = true;
890 if( !bPreviousOverlap && bNextOverlap )
892 pFirstBorder = pCurrent;
893 break;
895 pCurrent = pCurrent->pNext;
897 while( pCurrent != pStart );
899 if( !bOverlapFound )
900 return false;
902 ///in case we found a label (`pFirstBorder`) which overlaps with the next
903 ///label and not with the previous label a second while loop is started with
904 ///`pFirstBorder` as initial label; one more time the bounding box of each
905 ///label is checked for overlap against the bounding box of the previous and
906 ///of the next label, however this time we exit from the loop only if the
907 ///current label overlaps with the previous one but does not with the next
908 ///one (the opposite of what is required in the former loop);
909 ///in case such a label is found it is assigned to `pSecondBorder` and the
910 ///iteration is stopped; so in case there is a chain of overlapping labels
911 ///we end up having the first label of the chain pointed by `pFirstBorder`
912 ///and the last label of the chain pointed by `pSecondBorder`;
913 if( pFirstBorder )
915 pCurrent = pFirstBorder;
918 ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) );
919 ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap );
920 aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) );
921 aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) );
923 if( !aPreviousOverlap.isEmpty() && aNextOverlap.isEmpty() )
925 pSecondBorder = pCurrent;
926 break;
928 pCurrent = pCurrent->pNext;
930 while( pCurrent != pFirstBorder );
933 ///when two labels satisfying the required conditions are not found
934 ///(`pFirstBorder == 0 || pSecondBorder == 0`) but still an overlap occurs
935 ///(`bOverlapFound == true`) we are in the situation where each label
936 ///overlaps with both the previous and the next one; so `pFirstBorder` is
937 ///set to point to the last `PieLabelInfo` object in the collection and
938 ///`pSecondBorder` is set to point to the first one;
939 if( !pFirstBorder || !pSecondBorder )
941 pFirstBorder = &(*(m_aLabelInfoList.rbegin()));
942 pSecondBorder = &(*(m_aLabelInfoList.begin()));
945 ///the total number of labels that made up the chain is calculated and used
946 ///for getting a pointer to the central label (`pCenter`);
947 PieLabelInfo* pCenter = pFirstBorder;
948 sal_Int32 nOverlapGroupCount = 1;
949 for( pCurrent = pFirstBorder ;pCurrent != pSecondBorder; pCurrent = pCurrent->pNext )
950 nOverlapGroupCount++;
951 sal_Int32 nCenterPos = nOverlapGroupCount/2;
952 bool bSingleCenter = nOverlapGroupCount%2 != 0;
953 if( bSingleCenter )
954 nCenterPos++;
955 if(nCenterPos>1)
957 pCurrent = pFirstBorder;
958 while( --nCenterPos )
959 pCurrent = pCurrent->pNext;
960 pCenter = pCurrent;
963 ///the current position of each label in the collection is saved in
964 ///`PieLabelInfo.aPreviousPosition`, so that it is possible to undo the label
965 ///move action if it is needed; the undo action is provided by the
966 ///`PieChart::resetLabelPositionsToPreviousState` method.
967 pCurrent = pStart;
970 pCurrent->aPreviousPosition = pCurrent->xLabelGroupShape->getPosition();
971 pCurrent = pCurrent->pNext;
973 while( pCurrent != pStart );
975 ///the `PieChart::tryMoveLabels` method is invoked with
976 ///`rbAlternativeMoveDirection` boolean parameter set to false, such a method
977 ///tries to remove all overlaps that occur in the list of labels going from
978 ///`pFirstBorder` to `pSecondBorder`;
979 ///if the `PieChart::tryMoveLabels` returns true no further action is
980 ///performed, however it is worth noting that it does not mean that all
981 ///overlap issues have been surely fixed, but only that all moved labels are
982 ///at least completely inside the page document;
983 ///when `PieChart::tryMoveLabels` returns false, it means that the attempt
984 ///to fix one of the overlap issues caused that a label has been moved
985 ///(partially) outside the page document (anyway the `PieChart::tryMoveLabels`
986 ///method takes care to restore the position of all labels to their initial
987 ///position, and to set the `rbAlternativeMoveDirection` in/out parameter to
988 ///true); in such a case a second invocation of `PieChart::tryMoveLabels` is
989 ///performed (and this time the `rbAlternativeMoveDirection` boolean
990 ///parameter is true) and independently by what the `PieChart::tryMoveLabels`
991 ///method returns no further action is performed;
992 ///(see notes for `PieChart::tryMoveLabels`);
993 bool bAlternativeMoveDirection = false;
994 if( !tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize ) )
995 tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize );
997 ///in both cases (one or two invocations of `PieChart::tryMoveLabels`) the
998 ///`detectLabelOverlapsAndMove` method ends returning true.
999 return true;
1003 /** Try to remove all overlaps that occur in the list of labels going from
1004 * `pFirstBorder` to `pSecondBorder`
1006 bool PieChart::tryMoveLabels( PieLabelInfo const * pFirstBorder, PieLabelInfo const * pSecondBorder
1007 , PieLabelInfo* pCenter
1008 , bool bSingleCenter, bool& rbAlternativeMoveDirection, const awt::Size& rPageSize )
1011 PieLabelInfo* p1 = bSingleCenter ? pCenter->pPrevious : pCenter;
1012 PieLabelInfo* p2 = pCenter->pNext;
1013 //return true when successful
1015 bool bLabelOrderIsAntiClockWise = m_pPosHelper->isMathematicalOrientationAngle();
1017 ///two loops are performed simultaneously: the outer loop iterates on
1018 ///`PieLabelInfo` objects in the list starting from the central element
1019 ///(`pCenter`) and moving forward until the last element (`pSecondBorder`);
1020 ///the inner loop starts from the previous element of `pCenter` and moves
1021 ///forward until the current `PieLabelInfo` object of the outer loop is
1022 ///reached
1023 PieLabelInfo* pCurrent = nullptr;
1024 for( pCurrent = p2 ;pCurrent->pPrevious != pSecondBorder; pCurrent = pCurrent->pNext )
1026 PieLabelInfo* pFix = nullptr;
1027 for( pFix = p2->pPrevious ;pFix != pCurrent; pFix = pFix->pNext )
1029 ///on the current `PieLabelInfo` object of the outer loop the
1030 ///`moveAwayFrom` method is invoked by passing the current
1031 ///`PieLabelInfo` object of the inner loop as argument.
1033 ///so each label going from the central one to the last one is
1034 ///checked for overlapping against all previous labels (that comes
1035 ///after the central label) and in case the overlap occurs the
1036 ///`moveAwayFrom` method tries to fix the issue;
1037 ///if `moveAwayFrom` returns true (pay attention: that does not
1038 ///mean that the overlap issue has been surely fixed but only that
1039 ///the moved label is at least completely inside the page document:
1040 ///see notes on `PieChart::PieLabelInfo::moveAwayFrom`), the inner
1041 ///loop starts a new iteration else the `rbAlternativeMoveDirection`
1042 ///boolean parameter is tested: if it is false the parameter is set
1043 ///to true, the position of all labels is restored to the initial
1044 ///one (through the `PieChart::resetLabelPositionsToPreviousState`
1045 ///method) and the method ends by returning false, else the inner
1046 ///loop starts a new iteration step;
1047 ///so when `rbAlternativeMoveDirection` is true the method goes on
1048 ///trying to fix left overlap issues even if the last `moveAwayFrom`
1049 ///invocation has moved a label in a position that it is not
1050 ///completely inside the page document
1052 if( !pCurrent->moveAwayFrom( pFix, rPageSize, !bSingleCenter && pCurrent == p2, !bLabelOrderIsAntiClockWise ) )
1054 if( !rbAlternativeMoveDirection )
1056 rbAlternativeMoveDirection = true;
1057 resetLabelPositionsToPreviousState();
1058 return false;
1064 ///if the method does not return before ending the first pair of loops,
1065 ///a second pair of simultaneous loops is performed in the opposite
1066 ///direction (respect with the previous case): the outer loop iterates on
1067 ///`PieLabelInfo` objects in the list starting from the central element
1068 ///(`pCenter`) and moving backward until the first element (`pFirstBorder`);
1069 ///the inner loop starts from the next element of `pCenter` and moves
1070 ///backward until the current `PieLabelInfo` object of the outer loop is
1071 ///reached
1073 ///like in the previous case on the current `PieLabelInfo` object of
1074 ///the outer loop the `moveAwayFrom` method is invoked by passing
1075 ///the current `PieLabelInfo` object of the inner loop as argument
1077 ///so each label going from the central one to the first one is checked for
1078 ///overlapping on all subsequent labels (that come before the central label)
1079 ///and in case the overlap occurs the `moveAwayFrom` method tries to fix
1080 ///the issue. The subsequent actions performed after the invocation
1081 ///`moveAwayFrom` are the same detailed above for the first pair of loops
1083 for( pCurrent = p1 ;pCurrent->pNext != pFirstBorder; pCurrent = pCurrent->pPrevious )
1085 PieLabelInfo* pFix = nullptr;
1086 for( pFix = p2->pNext ;pFix != pCurrent; pFix = pFix->pPrevious )
1088 if( !pCurrent->moveAwayFrom( pFix, rPageSize, false, bLabelOrderIsAntiClockWise ) )
1090 if( !rbAlternativeMoveDirection )
1092 rbAlternativeMoveDirection = true;
1093 resetLabelPositionsToPreviousState();
1094 return false;
1099 return true;
1102 void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSize )
1104 ///this method is invoked by `ChartView::impl_createDiagramAndContent` for
1105 ///pie and donut charts after text label creation;
1106 ///it tries to rearrange labels only when the label placement type is
1107 ///`AVOID_OVERLAP`.
1108 // no need to do anything when we only have one label
1109 if (m_aLabelInfoList.size() < 2)
1110 return;
1112 ///check whether there are any labels that should be moved
1113 bool bMoveableFound = false;
1114 for (auto const& labelInfo : m_aLabelInfoList)
1116 if(labelInfo.bMovementAllowed)
1118 bMoveableFound = true;
1119 break;
1122 if(!bMoveableFound)
1123 return;
1125 double fPageDiagonaleLength = sqrt( double( rPageSize.Width*rPageSize.Width + rPageSize.Height*rPageSize.Height) );
1126 if( fPageDiagonaleLength == 0.0 )
1127 return;
1129 ///initialize next and previous member of `PieLabelInfo` objects
1130 auto aIt1 = m_aLabelInfoList.begin();
1131 auto aEnd = m_aLabelInfoList.end();
1132 std::vector< PieLabelInfo >::iterator aIt2 = aIt1;
1133 aIt1->pPrevious = &(*(m_aLabelInfoList.rbegin()));
1134 ++aIt2;
1135 for( ;aIt2!=aEnd; ++aIt1, ++aIt2 )
1137 PieLabelInfo& rInfo1( *aIt1 );
1138 PieLabelInfo& rInfo2( *aIt2 );
1139 rInfo1.pNext = &rInfo2;
1140 rInfo2.pPrevious = &rInfo1;
1142 aIt1->pNext = &(*(m_aLabelInfoList.begin()));
1144 ///detect overlaps and move
1145 sal_Int32 nMaxIterations = 50;
1146 while( detectLabelOverlapsAndMove( rPageSize ) && nMaxIterations > 0 )
1147 nMaxIterations--;
1149 ///create connection lines for the moved labels
1150 VLineProperties aVLineProperties;
1151 for (auto const& labelInfo : m_aLabelInfoList)
1153 if( labelInfo.bMoved )
1155 sal_Int32 nX1 = labelInfo.aFirstPosition.getX();
1156 sal_Int32 nY1 = labelInfo.aFirstPosition.getY();
1157 sal_Int32 nX2 = nX1;
1158 sal_Int32 nY2 = nY1;
1159 ::basegfx::B2IRectangle aRect( lcl_getRect( labelInfo.xLabelGroupShape ) );
1160 if( nX1 < aRect.getMinX() )
1161 nX2 = aRect.getMinX();
1162 else if( nX1 > aRect.getMaxX() )
1163 nX2 = aRect.getMaxX();
1165 if( nY1 < aRect.getMinY() )
1166 nY2 = aRect.getMinY();
1167 else if( nY1 > aRect.getMaxY() )
1168 nY2 = aRect.getMaxY();
1170 //when the line is very short compared to the page size don't create one
1171 ::basegfx::B2DVector aLength(nX1-nX2, nY1-nY2);
1172 if( (aLength.getLength()/fPageDiagonaleLength) < 0.01 )
1173 continue;
1175 drawing::PointSequenceSequence aPoints(1);
1176 aPoints[0].realloc(2);
1177 aPoints[0][0].X = nX1;
1178 aPoints[0][0].Y = nY1;
1179 aPoints[0][1].X = nX2;
1180 aPoints[0][1].Y = nY2;
1182 uno::Reference< beans::XPropertySet > xProp( labelInfo.xTextShape, uno::UNO_QUERY);
1183 if( xProp.is() )
1185 sal_Int32 nColor = 0;
1186 xProp->getPropertyValue("CharColor") >>= nColor;
1187 if( nColor != -1 )//automatic font color does not work for lines -> fallback to black
1188 aVLineProperties.Color <<= nColor;
1190 m_pShapeFactory->createLine2D( labelInfo.xTextTarget, aPoints, &aVLineProperties );
1196 /** Handle the placement of the label in the best fit case:
1197 * the routine try to place the label inside the related pie slice,
1198 * in case of success it returns true else returns false.
1200 * Notation:
1201 * C: the pie center
1202 * s: the bisector ray of the current pie slice
1203 * alpha: the angle between the horizontal axis and the bisector ray s
1204 * N: the vertex of the label b.b. which is nearest to C
1205 * F: the vertex of the label b.b. not adjacent to N; F lies on the pie border
1206 * P, Q: the intersection points between the label b.b. and the bisector ray s;
1207 * P is the one at minimum distance respect with C
1208 * e: the edge of the label b.b. where P lies (the nearest edge to C)
1209 * M: the vertex of e that is not N
1210 * G: the vertex of the label b.b. which is adjacent to N and that is not M
1211 * beta: the angle MPF
1212 * theta: the angle CPF
1216 * | /s
1217 * | /
1218 * | /
1219 * | G _________________________/____________________________ F
1220 * | | /Q ..|
1221 * | | / . . |
1222 * | | / . . |
1223 * | | / . . |
1224 * | | / . . |
1225 * | | / . . |
1226 * | | / d. . |
1227 * | | / . . |
1228 * | | / . . |
1229 * | | / . . |
1230 * | | / . . |
1231 * | | / . . |
1232 * | | / . . |
1233 * | | / . \ beta . |
1234 * | |__________/._\___|_______.____________________________|
1235 * | N /P / . M
1236 * | /___/theta .
1237 * | / .
1238 * | / . r
1239 * | / .
1240 * | / .
1241 * | / .
1242 * | / .
1243 * | / .
1244 * | / .
1245 * | / .
1246 * | / .
1247 * | /\. alpha
1248 * __|/__|_____________________________________________________________
1249 * |C
1253 * When alpha = 45k (k integer) s crosses the label b.b. at N exactly.
1254 * In such a case the nearest edge e is defined as the edge having N as the
1255 * start vertex and that is covered in the counterclockwise direction when
1256 * we move from N to the adjacent vertex.
1258 * The nearest vertex N is:
1259 * 1. the bottom left vertex when 0 < alpha < 90
1260 * 2. the bottom right vertex when 90 < alpha < 180
1261 * 3. the top right vertex when 180 < alpha < 270
1262 * 4. the top left vertex when 270 < alpha < 360.
1264 * The nearest edge e is:
1265 * 1. the left edge when −45 < alpha < 45
1266 * 2. the bottom edge when 45 < alpha <135
1267 * 3. the right edge when 135 < alpha < 225
1268 * 4. the top edge when 225 < alpha < 315.
1271 bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam, PieLabelInfo const & rPieLabelInfo)
1273 SAL_INFO( "chart2.pie.label.bestfit.inside",
1274 "** PieChart::performLabelBestFitInnerPlacement invoked **" );
1276 // get pie slice properties
1277 double fStartAngleDeg = lcl_getDegAngleInStandardRange(rShapeParam.mfUnitCircleStartAngleDegree);
1278 double fWidthAngleDeg = rShapeParam.mfUnitCircleWidthAngleDegree;
1279 double fHalfWidthAngleDeg = fWidthAngleDeg / 2.0;
1280 double fBisectingRayAngleDeg = lcl_getDegAngleInStandardRange(fStartAngleDeg + fHalfWidthAngleDeg);
1282 // get the middle point of the arc representing the pie slice border
1283 double fLogicZ = rShapeParam.mfLogicZ + 1.0;
1284 awt::Point aMiddleArcPoint = PlottingPositionHelper::transformSceneToScreenPosition(
1285 m_pPosHelper->transformUnitCircleToScene(
1286 fBisectingRayAngleDeg,
1287 rShapeParam.mfUnitCircleOuterRadius,
1288 fLogicZ ),
1289 m_xLogicTarget, m_pShapeFactory, m_nDimension );
1291 // compute the pie radius
1292 basegfx::B2IVector aPieCenter = rPieLabelInfo.aOrigin;
1293 basegfx::B2IVector aRadiusVector(
1294 aMiddleArcPoint.X - aPieCenter.getX(),
1295 aMiddleArcPoint.Y - aPieCenter.getY() );
1296 double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector);
1297 double fPieRadius = sqrt( fSquaredPieRadius );
1299 // the bb is moved as much as possible near to the border of the pie,
1300 // anyway a small offset from the border is present (0.025 * pie radius)
1301 const double fPieBorderOffset = 0.025;
1302 fPieRadius = fPieRadius - fPieRadius * fPieBorderOffset;
1304 SAL_INFO( "chart2.pie.label.bestfit.inside",
1305 " pie sector:" );
1306 SAL_INFO( "chart2.pie.label.bestfit.inside",
1307 " start angle = " << fStartAngleDeg );
1308 SAL_INFO( "chart2.pie.label.bestfit.inside",
1309 " angle width = " << fWidthAngleDeg );
1310 SAL_INFO( "chart2.pie.label.bestfit.inside",
1311 " bisecting ray angle = " << fBisectingRayAngleDeg );
1312 SAL_INFO( "chart2.pie.label.bestfit.inside",
1313 " pie radius = " << fPieRadius );
1314 SAL_INFO( "chart2.pie.label.bestfit.inside",
1315 " pie center = " << rPieLabelInfo.aOrigin );
1316 SAL_INFO( "chart2.pie.label.bestfit.inside",
1317 " middle arc point = (" << aMiddleArcPoint.X << ","
1318 << aMiddleArcPoint.Y << ")" );
1319 SAL_INFO( "chart2.pie.label.bestfit.inside",
1320 " label bounding box:" );
1321 SAL_INFO( "chart2.pie.label.bestfit.inside",
1322 " old anchor point = " << rPieLabelInfo.aFirstPosition );
1325 if( fPieRadius == 0.0 )
1326 return false;
1328 // get label b.b. width and height
1329 ::basegfx::B2IRectangle aBb( lcl_getRect( rPieLabelInfo.xLabelGroupShape ) );
1330 double fLabelWidth = aBb.getWidth();
1331 double fLabelHeight = aBb.getHeight();
1333 // -45 <= fAlphaDeg < 315
1334 double fAlphaDeg = lcl_getDegAngleInStandardRange(fBisectingRayAngleDeg + 45) - 45;
1335 double fAlphaRad = lcl_degToRad(fAlphaDeg);
1337 // compute nearest edge index
1338 // 0 left
1339 // 1 bottom
1340 // 2 right
1341 // 3 top
1342 int nSectorIndex = floor( (fAlphaDeg + 45) / 45.0 );
1343 int nNearestEdgeIndex = nSectorIndex / 2;
1345 // compute lengths of the nearest edge and of the orthogonal edges
1346 double fNearestEdgeLength = fLabelWidth;
1347 double fOrthogonalEdgeLength = fLabelHeight;
1348 int nAxisIndex = 0;
1349 int nOrthogonalAxisIndex = 1;
1350 if( nNearestEdgeIndex % 2 == 0 ) // nearest edge is vertical
1352 fNearestEdgeLength = fLabelHeight;
1353 fOrthogonalEdgeLength = fLabelWidth;
1354 nAxisIndex = 1;
1355 nOrthogonalAxisIndex = 0;
1358 // compute the distance between N and P
1359 // such a distance is piece wise linear respect with alpha:
1360 // given 45k <= alpha < 45(k+1) we have
1361 // when k is even: d(N,P) = (length(e) / 2) * (1 - (alpha - 45k)/45)
1362 // when k is odd: d(N,P) = (length(e) / 2) * (1 - (45(k+1) - alpha)/45)
1363 int nIndex = nSectorIndex -1; // nIndex = -1...6
1364 double fIndexMod2 = (nIndex + 8) % 2; // fIndexMod2 must be non negative
1365 double fSgn = 2.0 * (fIndexMod2 - 0.5); // 0 -> -1, 1 -> 1
1366 double fDistanceNP = (fNearestEdgeLength / 2.0) * (1 + fSgn * ((fAlphaDeg - 45 * (nIndex + fIndexMod2)) / 45.0));
1367 double fDistancePM = fNearestEdgeLength - fDistanceNP;
1369 // compute the length of the diagonal vector d,
1370 // that is the distance between P and F
1371 double fSquaredDistancePF = fDistancePM * fDistancePM + fOrthogonalEdgeLength * fOrthogonalEdgeLength;
1372 double fDistancePF = sqrt( fSquaredDistancePF );
1374 SAL_INFO( "chart2.pie.label.bestfit.inside",
1375 " width = " << fLabelWidth );
1376 SAL_INFO( "chart2.pie.label.bestfit.inside",
1377 " height = " << fLabelHeight );
1378 SAL_INFO( "chart2.pie.label.bestfit.inside",
1379 " nearest edge index = " << nNearestEdgeIndex );
1380 SAL_INFO( "chart2.pie.label.bestfit.inside",
1381 " alpha = " << fAlphaDeg );
1382 SAL_INFO( "chart2.pie.label.bestfit.inside",
1383 " distance(N,P) = " << fDistanceNP );
1384 SAL_INFO( "chart2.pie.label.bestfit.inside",
1385 " nIndex = " << nIndex );
1386 SAL_INFO( "chart2.pie.label.bestfit.inside",
1387 " fIndexMod2 = " << fIndexMod2 );
1388 SAL_INFO( "chart2.pie.label.bestfit.inside",
1389 " fSgn = " << fSgn );
1390 SAL_INFO( "chart2.pie.label.bestfit.inside",
1391 " distance(P,F) = " << fDistancePF );
1394 // we check that the condition length(d) <= pie radius holds
1395 if (fDistancePF > fPieRadius)
1397 return false;
1400 // compute beta: the angle of the diagonal vector d,
1401 // that is, the angle in P respect with the triangle PMF;
1402 // since both arguments are non negative the returned value is in [0, PI/2]
1403 double fBetaRad = atan2( fOrthogonalEdgeLength, fDistancePM );
1405 // compute the theta angle, that is the angle in P
1406 // respect with the triangle CFP;
1407 // when the second intersection edge is opposite to the nearest edge,
1408 // theta depends on alpha and beta according to the following relation:
1409 // theta = f(alpha, beta) = s * alpha + 90 * (1 - s * i) + beta
1410 // where i is the nearest edge index and s is the sign of (alpha' - 45),
1411 // with alpha' = (alpha + 45) mod 90;
1412 // when the second intersection edge is adjacent to the nearest edge,
1413 // we have theta = 360 - f(alpha, beta);
1414 // note that in the former case 0 <= f(alpha, beta) <= 180,
1415 // whilst in the latter case 180 <= f(alpha, beta) <= 360;
1416 double fAlphaMod90 = fmod( fAlphaDeg + 45, 90.0 ) - 45;
1417 double fSign = fAlphaMod90 == 0.0
1418 ? 0.0
1419 : ( fAlphaMod90 < 0 ) ? -1.0 : 1.0;
1420 double fThetaRad = fSign * fAlphaRad + M_PI_2 * (1 - fSign * nNearestEdgeIndex) + fBetaRad;
1421 if( fThetaRad > M_PI )
1423 fThetaRad = 2 * M_PI - fThetaRad;
1426 // compute the length of the positional vector,
1427 // that is the distance between C and P
1428 double fDistanceCP;
1429 // when the bisector ray intersects the b.b. in F we have theta mod 180 == 0
1430 if( fmod(fThetaRad, M_PI) == 0.0 )
1432 fDistanceCP = fPieRadius - fDistancePF;
1434 else // general case
1436 // we can compute d(C,P) by applying some trigonometric formula to
1437 // the triangle CFP : we know length(d) and length(r) = r and we have
1438 // computed the angle in P (theta); so named delta the angle in C and
1439 // gamma the angle in F, by the relation:
1441 // r d(P,F) d(C,P)
1442 // --------- = --------- = ---------
1443 // sin theta sin delta sin gamma
1445 // we get the wanted distance
1446 double fSinTheta = sin( fThetaRad );
1447 double fSinDelta = fDistancePF * fSinTheta / fPieRadius;
1448 double fDeltaRad = asin( fSinDelta );
1449 double fGammaRad = M_PI - (fThetaRad + fDeltaRad);
1450 double fSinGamma = sin( fGammaRad );
1451 fDistanceCP = fPieRadius * fSinGamma / fSinTheta;
1454 // define the positional vector
1455 basegfx::B2DVector aPositionalVector( cos(fAlphaRad), sin(fAlphaRad) );
1456 aPositionalVector.setLength(fDistanceCP);
1458 // we define a direction vector in order to know
1459 // in which quadrant we are working
1460 basegfx::B2DVector aDirection(1.0, 1.0);
1461 if( 90 <= fBisectingRayAngleDeg && fBisectingRayAngleDeg < 270 )
1463 aDirection.setX(-1.0);
1465 if( fBisectingRayAngleDeg >= 180 )
1467 aDirection.setY(-1.0);
1470 // compute vertices N, M and G respect with pie center C
1471 basegfx::B2DVector aNearestVertex(aPositionalVector);
1472 aNearestVertex[nAxisIndex] += -aDirection[nAxisIndex] * fDistanceNP;
1473 basegfx::B2DVector aVertexM(aNearestVertex);
1474 aVertexM[nAxisIndex] += aDirection[nAxisIndex] * fNearestEdgeLength;
1475 basegfx::B2DVector aVertexG(aNearestVertex);
1476 aVertexG[nOrthogonalAxisIndex] += aDirection[nOrthogonalAxisIndex] * fOrthogonalEdgeLength;
1478 SAL_INFO( "chart2.pie.label.bestfit.inside",
1479 " beta = " << lcl_radToDeg(fBetaRad) );
1480 SAL_INFO( "chart2.pie.label.bestfit.inside",
1481 " theta = " << lcl_radToDeg(fThetaRad) );
1482 SAL_INFO( "chart2.pie.label.bestfit.inside",
1483 " fAlphaMod90 = " << fAlphaMod90 );
1484 SAL_INFO( "chart2.pie.label.bestfit.inside",
1485 " fSign = " << fSign );
1486 SAL_INFO( "chart2.pie.label.bestfit.inside",
1487 " distance(C,P) = " << fDistanceCP );
1488 SAL_INFO( "chart2.pie.label.bestfit.inside",
1489 " direction vector = " << aDirection );
1490 SAL_INFO( "chart2.pie.label.bestfit.inside",
1491 " N = " << aNearestVertex );
1492 SAL_INFO( "chart2.pie.label.bestfit.inside",
1493 " M = " << aVertexM );
1494 SAL_INFO( "chart2.pie.label.bestfit.inside",
1495 " G = " << aVertexG );
1497 // in order to be able to place the label inside the pie slice we need
1498 // to check that each angle between s and the ray starting from C and
1499 // passing through a b.b. vertex is less than half width of the pie slice;
1500 // when the nearest edge e crosses a Cartesian axis it is sufficient
1501 // to test only the vertices belonging to e, else we need to test
1502 // the 2 vertices that aren’t either N or F . Note that if a b.b. edge
1503 // crosses a Cartesian axis then it is the nearest edge to C
1505 // check the angle between CP and CM
1506 double fAngleRad = aPositionalVector.angle(aVertexM);
1507 double fAngleDeg = lcl_getDegAngleInStandardRange( lcl_radToDeg(fAngleRad) );
1508 if( fAngleDeg > 180 ) // in case the wrong angle has been computed
1509 fAngleDeg = 360 - fAngleDeg;
1510 SAL_INFO( "chart2.pie.label.bestfit.inside",
1511 " angle between CP and CM: " << fAngleDeg );
1512 if( fAngleDeg > fHalfWidthAngleDeg )
1514 return false;
1517 if( ( aNearestVertex[nAxisIndex] >= 0 && aVertexM[nAxisIndex] <= 0 )
1518 || ( aNearestVertex[nAxisIndex] <= 0 && aVertexM[nAxisIndex] >= 0 ) )
1520 // check the angle between CP and CN
1521 fAngleRad = aPositionalVector.angle(aNearestVertex);
1522 fAngleDeg = lcl_getDegAngleInStandardRange( lcl_radToDeg(fAngleRad) );
1523 if( fAngleDeg > 180 ) // in case the wrong angle has been computed
1524 fAngleDeg = 360 - fAngleDeg;
1525 SAL_INFO( "chart2.pie.label.bestfit.inside",
1526 " angle between CP and CN: " << fAngleDeg );
1527 if( fAngleDeg > fHalfWidthAngleDeg )
1529 return false;
1532 else
1534 // check the angle between CP and CG
1535 fAngleRad = aPositionalVector.angle(aVertexG);
1536 fAngleDeg = lcl_getDegAngleInStandardRange( lcl_radToDeg(fAngleRad) );
1537 if( fAngleDeg > 180 ) // in case the wrong angle has been computed
1538 fAngleDeg = 360 - fAngleDeg;
1539 SAL_INFO( "chart2.pie.label.bestfit.inside",
1540 " angle between CP and CG: " << fAngleDeg );
1541 if( fAngleDeg > fHalfWidthAngleDeg )
1543 return false;
1547 // compute the b.b. center respect with the pie center
1548 basegfx::B2DVector aBBCenter(aNearestVertex);
1549 aBBCenter[nAxisIndex] += aDirection[nAxisIndex] * fNearestEdgeLength / 2;
1550 aBBCenter[nOrthogonalAxisIndex] += aDirection[nOrthogonalAxisIndex] * fOrthogonalEdgeLength / 2;
1552 // compute the b.b. anchor point
1553 basegfx::B2IVector aNewAnchorPoint = aPieCenter;
1554 aNewAnchorPoint[0] += floor(aBBCenter[0]);
1555 aNewAnchorPoint[1] -= floor(aBBCenter[1]); // the Y axis on the screen points downward
1557 // compute the translation vector for moving the label from the current
1558 // screen position to the new one
1559 basegfx::B2IVector aTranslationVector = aNewAnchorPoint - rPieLabelInfo.aFirstPosition;
1561 // compute the new screen position and move the label
1562 // XShape::getPosition returns the top left vertex of the b.b. of the shape
1563 awt::Point aOldPos( rPieLabelInfo.xLabelGroupShape->getPosition() );
1564 awt::Point aNewPos( aOldPos.X + aTranslationVector.getX(),
1565 aOldPos.Y + aTranslationVector.getY() );
1566 rPieLabelInfo.xLabelGroupShape->setPosition(aNewPos);
1568 SAL_INFO( "chart2.pie.label.bestfit.inside",
1569 " center = " << aBBCenter );
1570 SAL_INFO( "chart2.pie.label.bestfit.inside",
1571 " new anchor point = " << aNewAnchorPoint );
1572 SAL_INFO( "chart2.pie.label.bestfit.inside",
1573 " translation vector = " << aTranslationVector );
1574 SAL_INFO( "chart2.pie.label.bestfit.inside",
1575 " old position = (" << aOldPos.X << "," << aOldPos.Y << ")" );
1576 SAL_INFO( "chart2.pie.label.bestfit.inside",
1577 " new position = (" << aNewPos.X << "," << aNewPos.Y << ")" );
1579 return true;
1582 /** Handle the placement of the label in the best fit case.
1583 * First off the routine try to place the label inside the related pie slice,
1584 * if this is not possible the label is placed outside.
1586 void PieChart::performLabelBestFit(ShapeParam& rShapeParam, PieLabelInfo const & rPieLabelInfo)
1588 if( m_bUseRings )
1589 return;
1591 if( !performLabelBestFitInnerPlacement(rShapeParam, rPieLabelInfo) )
1593 // If it does not fit inside, let's put it outside
1594 PolarLabelPositionHelper aPolarPosHelper(m_pPosHelper.get(),m_nDimension,m_xLogicTarget,m_pShapeFactory);
1595 auto eAlignment = LABEL_ALIGN_CENTER;
1596 awt::Point aScreenPosition2D(
1597 aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(eAlignment, css::chart::DataLabelPlacement::OUTSIDE
1598 , rShapeParam.mfUnitCircleStartAngleDegree, rShapeParam.mfUnitCircleWidthAngleDegree
1599 , rShapeParam.mfUnitCircleInnerRadius, rShapeParam.mfUnitCircleOuterRadius, rShapeParam.mfLogicZ+0.5, 0 ));
1600 basegfx::B2IVector aTranslationVector = rPieLabelInfo.aFirstPosition - rPieLabelInfo.aOrigin;
1601 aTranslationVector.setLength(150);
1602 aScreenPosition2D.X += aTranslationVector.getX();
1603 aScreenPosition2D.Y += aTranslationVector.getY();
1604 rPieLabelInfo.xLabelGroupShape->setPosition(aScreenPosition2D);
1608 } //namespace chart
1610 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */