xref: /core/chart2/source/view/charttypes/PieChart.cxx (revision be6015e3c4e3fcbb2a7c08d30156be71cfca768b)
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
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/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
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 .
18  */
19 
20 #include <BaseGFXHelper.hxx>
21 #include <VLineProperties.hxx>
22 #include "PieChart.hxx"
23 #include <ShapeFactory.hxx>
24 #include <PolarLabelPositionHelper.hxx>
25 #include <CommonConverters.hxx>
26 #include <ObjectIdentifier.hxx>
27 #include <ChartType.hxx>
28 #include <DataSeries.hxx>
29 #include <DataSeriesProperties.hxx>
30 #include "../../model/main/DataPointProperties.hxx"
31 #include <LinePropertiesHelper.hxx>
32 #include <com/sun/star/chart/DataLabelPlacement.hpp>
33 #include <com/sun/star/chart2/XColorScheme.hpp>
34 
35 #include <com/sun/star/drawing/XShapes.hpp>
36 #include <sal/log.hxx>
37 #include <osl/diagnose.h>
38 #include <comphelper/diagnose_ex.hxx>
39 #include <o3tl/untaint.hxx>
40 #include <tools/helpers.hxx>
41 
42 #include <limits>
43 #include <memory>
44 
45 using namespace ::com::sun::star;
46 using namespace ::com::sun::star::chart2;
47 using namespace ::chart::DataSeriesProperties;
48 
49 namespace chart {
50 
51 struct PieChart::ShapeParam
52 {
53     /** the start angle of the slice
54      */
55     double mfUnitCircleStartAngleDegree;
56 
57     /** the angle width of the slice
58      */
59     double mfUnitCircleWidthAngleDegree;
60 
61     /** the normalized outer radius of the ring the slice belongs to.
62      */
63     double mfUnitCircleOuterRadius;
64 
65     /** the normalized inner radius of the ring the slice belongs to
66      */
67     double mfUnitCircleInnerRadius;
68 
69     /** relative distance offset of a slice from the pie center;
70      *  this parameter is used for instance when the user performs manual
71      *  dragging of a slice (the drag operation is possible only for slices that
72      *  belong to the outer ring and only along the ray bisecting the slice);
73      *  the value for the given entry in the data series is obtained by the
74      *  `Offset` property attached to each entry; note that the value
75      *  provided by the `Offset` property is used both as a logical value in
76      *  `PiePositionHelper::getInnerAndOuterRadius` and as a percentage value in
77      *  the `PieChart::createDataPoint` and `PieChart::createTextLabelShape`
78      *  methods; since the logical height of a ring is always 1, this duality
79      *  does not cause any incorrect behavior;
80      */
81     double mfExplodePercentage;
82 
83     /** sum of all Y values in a single series
84      */
85     double mfLogicYSum;
86 
87     /** for 3D pie chart: label z coordinate
88      */
89     double mfLogicZ;
90 
91     /** for 3D pie chart: height
92      */
93     double mfDepth;
94 
ShapeParamchart::PieChart::ShapeParam95     ShapeParam() :
96         mfUnitCircleStartAngleDegree(0.0),
97         mfUnitCircleWidthAngleDegree(0.0),
98         mfUnitCircleOuterRadius(0.0),
99         mfUnitCircleInnerRadius(0.0),
100         mfExplodePercentage(0.0),
101         mfLogicYSum(0.0),
102         mfLogicZ(0.0),
103         mfDepth(0.0) {}
104 };
105 
106 namespace
107 {
lcl_getRect(const rtl::Reference<SvxShape> & xShape)108 ::basegfx::B2IRectangle lcl_getRect(const rtl::Reference<SvxShape>& xShape)
109 {
110     ::basegfx::B2IRectangle aRect;
111     if (xShape.is())
112         aRect = BaseGFXHelper::makeRectangle(xShape->getPosition(), xShape->getSize());
113     return aRect;
114 }
115 
lcl_isInsidePage(const awt::Point & rPos,const awt::Size & rSize,const awt::Size & rPageSize)116 bool lcl_isInsidePage(const awt::Point& rPos, const awt::Size& rSize, const awt::Size& rPageSize)
117 {
118     if (rPos.X < 0 || rPos.Y < 0)
119         return false;
120     if ((rPos.X + rSize.Width) > rPageSize.Width)
121         return false;
122     if ((rPos.Y + rSize.Height) > rPageSize.Height)
123         return false;
124     return true;
125 }
126 
127 } //end anonymous namespace
128 
PiePositionHelper(double fAngleDegreeOffset)129 PiePositionHelper::PiePositionHelper( double fAngleDegreeOffset )
130         : m_fRingDistance(0.0)
131 {
132     m_fRadiusOffset = 0.0;
133     m_fAngleDegreeOffset = fAngleDegreeOffset;
134 }
135 
136 /** Compute the outer and the inner radius for the current ring (not for the
137  *  whole donut!), in general it is:
138  *      inner_radius = (ring_index + 1) - 0.5 + max_offset,
139  *      outer_radius = (ring_index + 1) + 0.5 + max_offset.
140  *  When orientation for the radius axis is reversed these values are swapped.
141  *  (Indeed the orientation for the radius axis is always reversed!
142  *  See `PieChartTypeTemplate::adaptScales`.)
143  *  The maximum relative offset (see notes for `PieChart::getMaxOffset`) is
144  *  added to both the inner and the outer radius.
145  *  It returns true if the ring is visible (that is not out of the radius
146  *  axis scale range).
147  */
getInnerAndOuterRadius(double fCategoryX,double & fLogicInnerRadius,double & fLogicOuterRadius,bool bUseRings,double fMaxOffset) const148 bool PiePositionHelper::getInnerAndOuterRadius( double fCategoryX
149                                                , double& fLogicInnerRadius, double& fLogicOuterRadius
150                                                , bool bUseRings, double fMaxOffset ) const
151 {
152     if( !bUseRings )
153         fCategoryX = 1.0;
154 
155     double fLogicInner = fCategoryX -0.5+m_fRingDistance/2.0;
156     double fLogicOuter = fCategoryX +0.5-m_fRingDistance/2.0;
157 
158     if( !isMathematicalOrientationRadius() )
159     {
160         //in this case the given getMaximumX() was not correct instead the minimum should have been smaller by fMaxOffset
161         //but during getMaximumX and getMimumX we do not know the axis orientation
162         fLogicInner += fMaxOffset;
163         fLogicOuter += fMaxOffset;
164     }
165 
166     if( fLogicInner >= getLogicMaxX() )
167         return false;
168     if( fLogicOuter <= getLogicMinX() )
169         return false;
170 
171     if( fLogicInner < getLogicMinX() )
172         fLogicInner = getLogicMinX();
173     if( fLogicOuter > getLogicMaxX() )
174         fLogicOuter = getLogicMaxX();
175 
176     fLogicInnerRadius = fLogicInner;
177     fLogicOuterRadius = fLogicOuter;
178     if( !isMathematicalOrientationRadius() )
179         std::swap(fLogicInnerRadius,fLogicOuterRadius);
180     return true;
181 }
182 
183 
clockwiseWedges() const184 bool PiePositionHelper::clockwiseWedges() const
185 {
186     const ExplicitScaleData& rAngleScale = m_bSwapXAndY ? m_aScales[1] : m_aScales[0];
187     return rAngleScale.Orientation == AxisOrientation_REVERSE;
188 }
189 
190 
PieChart(const rtl::Reference<ChartType> & xChartTypeModel,sal_Int32 nDimensionCount,bool bExcludingPositioning)191 PieChart::PieChart( const rtl::Reference<ChartType>& xChartTypeModel
192                    , sal_Int32 nDimensionCount
193                    , bool bExcludingPositioning )
194         : VSeriesPlotter( xChartTypeModel, nDimensionCount )
195         , m_aPosHelper( (m_nDimension==3) ? 0.0 : 90.0 )
196         , m_bUseRings(false)
197         , m_bSizeExcludesLabelsAndExplodedSegments(bExcludingPositioning)
198         , m_eSubType(PieChartSubType_NONE)
199         , m_nSplitPos(2)
200         , m_fMaxOffset(std::numeric_limits<double>::quiet_NaN())
201 {
202     PlotterBase::m_pPosHelper = &m_aPosHelper;
203     VSeriesPlotter::m_pMainPosHelper = &m_aPosHelper;
204     m_aPosHelper.m_fRadiusOffset = 0.0;
205     m_aPosHelper.m_fRingDistance = 0.0;
206 
207     if( !xChartTypeModel.is() )
208         return;
209 
210     try
211     {
212         xChartTypeModel->getFastPropertyValue(PROP_PIECHARTTYPE_USE_RINGS) >>= m_bUseRings; //  "UseRings"
213         if( m_bUseRings )
214         {
215             m_aPosHelper.m_fRadiusOffset = 1.0;
216             if( nDimensionCount==3 )
217                 m_aPosHelper.m_fRingDistance = 0.1;
218         }
219     }
220     catch( const uno::Exception& )
221     {
222         TOOLS_WARN_EXCEPTION("chart2", "" );
223     }
224     try
225     {
226         xChartTypeModel->getFastPropertyValue(PROP_PIECHARTTYPE_SUBTYPE) >>= m_eSubType; //  "SubType"
227     }
228     catch( const uno::Exception& )
229     {
230         TOOLS_WARN_EXCEPTION("chart2", "" );
231     }
232     try
233     {
234         xChartTypeModel->getFastPropertyValue(PROP_PIECHARTTYPE_SPLIT_POS) >>= m_nSplitPos; //  "CompositeSize"
235     }
236     catch( const uno::Exception& )
237     {
238         TOOLS_WARN_EXCEPTION("chart2", "" );
239     }
240 }
241 
~PieChart()242 PieChart::~PieChart()
243 {
244 }
245 
setScales(std::vector<ExplicitScaleData> && rScales,bool)246 void PieChart::setScales( std::vector< ExplicitScaleData >&& rScales, bool /* bSwapXAndYAxis */ )
247 {
248     OSL_ENSURE(m_nDimension<=static_cast<sal_Int32>(rScales.size()),"Dimension of Plotter does not fit two dimension of given scale sequence");
249     m_aPosHelper.setScales( std::move(rScales), true );
250 }
251 
getPreferredDiagramAspectRatio() const252 drawing::Direction3D PieChart::getPreferredDiagramAspectRatio() const
253 {
254     if( m_nDimension == 3 )
255         return drawing::Direction3D(1,1,0.10);
256     return drawing::Direction3D(1,1,1);
257 }
258 
shouldSnapRectToUsedArea()259 bool PieChart::shouldSnapRectToUsedArea()
260 {
261     return true;
262 }
263 
createDataPoint(const SubPieType e_subType,const rtl::Reference<SvxShapeGroupAnyD> & xTarget,const uno::Reference<beans::XPropertySet> & xObjectProperties,const ShapeParam & rParam,const sal_Int32 nPointCount,const bool bConcentricExplosion)264 rtl::Reference<SvxShape> PieChart::createDataPoint(
265     const SubPieType e_subType,
266     const rtl::Reference<SvxShapeGroupAnyD>& xTarget,
267     const uno::Reference<beans::XPropertySet>& xObjectProperties,
268     const ShapeParam& rParam,
269     const sal_Int32 nPointCount,
270     const bool bConcentricExplosion)
271 {
272     //transform position:
273     drawing::Direction3D aOffset;
274     double fExplodedInnerRadius = rParam.mfUnitCircleInnerRadius;
275     double fExplodedOuterRadius = rParam.mfUnitCircleOuterRadius;
276     double fStartAngle = rParam.mfUnitCircleStartAngleDegree;
277     double fWidthAngle = rParam.mfUnitCircleWidthAngleDegree;
278 
279     if (rParam.mfExplodePercentage != 0.0) {
280         double fRadius = (fExplodedOuterRadius-fExplodedInnerRadius)*rParam.mfExplodePercentage;
281 
282         if (bConcentricExplosion) {
283 
284             // For concentric explosion, increase the radius but retain the original
285             // arc length of all ring segments together. This results in a gap
286             // that's evenly divided among all segments, assuming they all have
287             // the same explosion percentage
288             assert(fExplodedInnerRadius >= 0 && fExplodedOuterRadius > 0);
289             double fAngleRatio = (fExplodedInnerRadius + fExplodedOuterRadius) /
290                 (fExplodedInnerRadius + fExplodedOuterRadius + 2 * fRadius);
291 
292             assert(nPointCount > 0);
293             double fAngleGap = 360 * (1.0 - fAngleRatio) / nPointCount;
294             fStartAngle += fAngleGap / 2;
295             fWidthAngle -= fAngleGap;
296 
297             fExplodedInnerRadius += fRadius;
298             fExplodedOuterRadius += fRadius;
299 
300         } else {
301             // For the non-concentric explosion case, keep the original radius
302             // but shift the circle origin
303             double fAngle  = fStartAngle + fWidthAngle/2.0;
304 
305             drawing::Position3D aOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ);
306             drawing::Position3D aNewOrigin = m_aPosHelper.transformUnitCircleToScene(fAngle, fRadius, rParam.mfLogicZ);
307             aOffset = aNewOrigin - aOrigin;
308         }
309     } else {
310         drawing::Position3D aOrigin, aNewOrigin;
311         switch (e_subType) {
312             case SubPieType::LEFT:
313                 // Draw the main pie for bar-of-pie/pie-of-pie smaller and to the left
314                 aOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ);
315                 aNewOrigin = m_aPosHelper.transformUnitCircleToScene(180, 0.75, rParam.mfLogicZ);
316                 aOffset = aNewOrigin - aOrigin;
317                 fExplodedOuterRadius *= m_fLeftScale;
318                 break;
319             case SubPieType::RIGHT:
320                 // Draw the sub-pie for pie-of-pie much smaller and to the right
321                 aOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ);
322                 aNewOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0.75, rParam.mfLogicZ);
323                 aOffset = aNewOrigin - aOrigin;
324                 fExplodedOuterRadius *= m_fRightScale;
325                 break;
326             case SubPieType::NONE:
327             default:
328                 // no change
329                 break;
330         }
331     }
332 
333 
334     //create point
335     rtl::Reference<SvxShape> xShape;
336     if(m_nDimension==3)
337     {
338         xShape = ShapeFactory::createPieSegment( xTarget
339             , fStartAngle, fWidthAngle
340             , fExplodedInnerRadius, fExplodedOuterRadius
341             , aOffset, B3DHomMatrixToHomogenMatrix( m_aPosHelper.getUnitCartesianToScene() )
342             , rParam.mfDepth );
343     }
344     else
345     {
346         xShape = ShapeFactory::createPieSegment2D( xTarget
347             , fStartAngle, fWidthAngle
348             , fExplodedInnerRadius, fExplodedOuterRadius
349             , aOffset, B3DHomMatrixToHomogenMatrix( m_aPosHelper.getUnitCartesianToScene() ) );
350     }
351     PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() );
352     return xShape;
353 }
354 
createBarDataPoint(const rtl::Reference<SvxShapeGroupAnyD> & xTarget,const uno::Reference<beans::XPropertySet> & xObjectProperties,const ShapeParam & rParam,double fBarSegBottom,double fBarSegTop)355 rtl::Reference<SvxShape> PieChart::createBarDataPoint(
356         const rtl::Reference<SvxShapeGroupAnyD>& xTarget,
357         const uno::Reference<beans::XPropertySet>& xObjectProperties,
358         const ShapeParam& rParam,
359         double fBarSegBottom, double fBarSegTop)
360 {
361     // Draw the bar for bar-of-pie small and to the right. Width and
362     // position are hard-coded for now.
363 
364     css::awt::Point aPos;
365     css::awt::Size aSz;
366 
367     getBarRect(&aPos, &aSz, fBarSegBottom, fBarSegTop, rParam);
368 
369     const tNameSequence emptyNameSeq;
370     const tAnySequence emptyValSeq;
371     //create point
372     rtl::Reference<SvxShape> xShape = ShapeFactory::createRectangle(
373             xTarget,
374             aSz, aPos,
375             emptyNameSeq, emptyValSeq);
376 
377     PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() );
378     return xShape;
379 }
380 
getBarRect(css::awt::Point * pPos,css::awt::Size * pSz,double fBarBottom,double fBarTop,const ShapeParam & rParam) const381 void PieChart::getBarRect(css::awt::Point *pPos, css::awt::Size *pSz,
382         double fBarBottom, double fBarTop, const ShapeParam& rParam) const
383 {
384     double x0 = m_aPosHelper.transformUnitCircleToScene(0, m_fBarLeft, 0).PositionX;
385     double x1 = m_aPosHelper.transformUnitCircleToScene(0, m_fBarRight, 0).PositionX;
386     double y0 = m_aPosHelper.transformUnitCircleToScene(
387             90, fBarBottom, 0).PositionY;
388     double y1 = m_aPosHelper.transformUnitCircleToScene(
389             90, fBarTop, 0).PositionY;
390 
391     drawing::Position3D aP0(x0, y0, rParam.mfLogicZ);
392     drawing::Position3D aP1(x1, y1, rParam.mfLogicZ);
393 
394     *pPos = css::awt::Point(aP0.PositionX, aP1.PositionY);
395     *pSz = css::awt::Size(fabs(aP0.PositionX - aP1.PositionX),
396             fabs(aP0.PositionY - aP1.PositionY));
397 }
398 
createTextLabelShape(const rtl::Reference<SvxShapeGroupAnyD> & xTextTarget,VDataSeries & rSeries,sal_Int32 nPointIndex,ShapeParam & rParam,enum SubPieType eType)399 void PieChart::createTextLabelShape(
400     const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget,
401     VDataSeries& rSeries, sal_Int32 nPointIndex, ShapeParam& rParam ,
402     enum SubPieType eType)
403 {
404     if (!rSeries.getDataPointLabelIfLabel(nPointIndex))
405         // There is no text label for this data point.  Nothing to do.
406         return;
407 
408     ///by using the `mfExplodePercentage` parameter a normalized offset is added
409     ///to both normalized radii. (See notes for
410     ///`PolarPlottingPositionHelper::transformToRadius`, especially example 3,
411     ///and related comments).
412     if (rParam.mfExplodePercentage != 0.0)
413     {
414         double fExplodeOffset = (rParam.mfUnitCircleOuterRadius-rParam.mfUnitCircleInnerRadius)*rParam.mfExplodePercentage;
415         rParam.mfUnitCircleInnerRadius += fExplodeOffset;
416         rParam.mfUnitCircleOuterRadius += fExplodeOffset;
417     }
418 
419     ///get the required label placement type. Available placements are
420     ///`AVOID_OVERLAP`, `CENTER`, `OUTSIDE` and `INSIDE`;
421     sal_Int32 nLabelPlacement = rSeries.getLabelPlacement(
422         nPointIndex, m_xChartTypeModel, m_aPosHelper.isSwapXAndY());
423 
424     // has an X/Y offset (relative to the OUTSIDE label default position) been provided?
425     const bool bHasCustomLabelPlacement = nLabelPlacement == css::chart::DataLabelPlacement::CUSTOM;
426     if (bHasCustomLabelPlacement)
427         nLabelPlacement = css::chart::DataLabelPlacement::OUTSIDE;
428 
429     ///when the placement is of `AVOID_OVERLAP` type a later rearrangement of
430     ///the label position is allowed; the `createTextLabelShape` treats the
431     ///`AVOID_OVERLAP` as if it was of `CENTER` type;
432 
433     double nVal = rSeries.getYValue(nPointIndex);
434     //AVOID_OVERLAP is in fact "Best fit" in the UI.
435     bool bMovementAllowed = nLabelPlacement == css::chart::DataLabelPlacement::AVOID_OVERLAP;
436     if( bMovementAllowed )
437         nLabelPlacement = css::chart::DataLabelPlacement::CENTER;
438 
439     ///for `OUTSIDE` (`INSIDE`) label placements an offset of 150 (-150), in the
440     ///radius direction, is added to the final screen position of the label
441     ///anchor point. This is required in order to ensure that the label is
442     ///completely outside (inside) the related slice. Indeed this value should
443     ///depend on the font height;
444     ///pay attention: 150 is not a big offset, in fact the screen position
445     ///coordinates for label anchor points are in the 10000-20000 range, hence
446     ///these are coordinates of a virtual screen and 150 is a small value;
447     LabelAlignment eAlignment(LABEL_ALIGN_CENTER);
448     sal_Int32 nScreenValueOffsetInRadiusDirection = 0 ;
449     if( nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE )
450         nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? 150 : 0;//todo maybe calculate this font height dependent
451     else if( nLabelPlacement == css::chart::DataLabelPlacement::INSIDE )
452         nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? -150 : 0;//todo maybe calculate this font height dependent
453 
454     double fRadiusScale;
455     double fXShift;
456     switch (eType) {
457     case SubPieType::LEFT:
458         fRadiusScale = m_fLeftScale;
459         fXShift = m_fLeftShift;
460         break;
461     case SubPieType::RIGHT:
462         fRadiusScale = m_fRightScale;
463         fXShift = m_fRightShift;
464         break;
465     default:
466         fRadiusScale = 1.0;
467         fXShift = 0;
468     }
469 
470     ::basegfx::B3DVector aShift(fXShift, 0, 0);
471 
472     ///the scene position of the label anchor point is calculated (see notes for
473     ///`PolarLabelPositionHelper::getLabelScreenPositionAndAlignmentForUnitCircleValues`),
474     ///and immediately transformed into the screen position.
475     PolarLabelPositionHelper aPolarPosHelper(&m_aPosHelper,m_nDimension,m_xLogicTarget);
476     awt::Point aScreenPosition2D(
477         aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(eAlignment, nLabelPlacement
478         , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree
479         , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius * fRadiusScale
480         , rParam.mfLogicZ+0.5, 0, aShift));
481 
482     ///the screen position of the pie/donut center is calculated.
483     PieLabelInfo aPieLabelInfo;
484     aPieLabelInfo.aFirstPosition = basegfx::B2IVector( aScreenPosition2D.X, aScreenPosition2D.Y );
485     awt::Point aOrigin( aPolarPosHelper.transformSceneToScreenPosition(
486                 m_aPosHelper.transformUnitCircleToScene( 0.0, 0.0,
487                     rParam.mfLogicZ+1.0, aShift ) ) );
488     aPieLabelInfo.aOrigin = basegfx::B2IVector( aOrigin.X, aOrigin.Y );
489 
490     ///add a scaling independent Offset if requested
491     if( nScreenValueOffsetInRadiusDirection != 0)
492     {
493         basegfx::B2IVector aDirection( aScreenPosition2D.X- aOrigin.X, aScreenPosition2D.Y- aOrigin.Y );
494         aDirection.setLength(nScreenValueOffsetInRadiusDirection);
495         aScreenPosition2D.X += aDirection.getX();
496         aScreenPosition2D.Y += aDirection.getY();
497     }
498 
499    // compute outer pie radius
500     awt::Point aOuterCirclePoint = PlottingPositionHelper::transformSceneToScreenPosition(
501             m_aPosHelper.transformUnitCircleToScene(
502                     0,
503                     rParam.mfUnitCircleOuterRadius * fRadiusScale,
504                     0 ,
505                     aShift),
506             m_xLogicTarget, m_nDimension );
507     basegfx::B2IVector aRadiusVector(
508             aOuterCirclePoint.X - aPieLabelInfo.aOrigin.getX(),
509             aOuterCirclePoint.Y - aPieLabelInfo.aOrigin.getY() );
510     double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector);
511     double fPieRadius = sqrt( fSquaredPieRadius );
512     const double fHalfWidthAngleDegree = rParam.mfUnitCircleWidthAngleDegree / 2.0;
513     // fAngleDegree: the angle through the center of the slice / the bisecting ray
514     const double fAngleDegree
515         = NormAngle360(rParam.mfUnitCircleStartAngleDegree + fHalfWidthAngleDegree);
516 
517     // aOuterPosition: slice midpoint on the circumference,
518     // which is where an outside/custom label would be connected
519     awt::Point aOuterPosition = PlottingPositionHelper::transformSceneToScreenPosition(
520         m_aPosHelper.transformUnitCircleToScene(fAngleDegree,
521             rParam.mfUnitCircleOuterRadius * fRadiusScale, 0, aShift),
522         m_xLogicTarget, m_nDimension);
523     aPieLabelInfo.aOuterPosition = basegfx::B2IVector(aOuterPosition.X, aOuterPosition.Y);
524 
525     /* There are basically three places where a label could be placed in a pie chart
526      * 1.) outside the slice
527      *      -typically used for long labels or charts with many, thin slices
528      * 2.) inside the slice (center or edge)
529      *      -typically used for charts with 5 or less slices
530      * 3.) in a custom location
531      *      -typically set (by auto-positioning I presume) when labels overlap
532      *
533      * Selecting a good width for the text is critical to achieving good-looking labels.
534      * Our bestFit algorithm completely depends on a good starting guess.
535      * Lots of room for improvement here...
536      * Warning: complication due to 3D ovals (so can't use normal circle functions),
537      * donuts(m_bUseRings), auto re-scaling of the pie chart, etc.
538      *
539      * Based on observation, Microsoft uses 1/5 of the chart space as its text limit,
540      * although it will reduce the width (as long as it is not a custom position)
541      * if doing so means that the now-taller-text will fit inside the slice,
542      * so best if we do the same for our charts.
543      */
544 
545     // set the maximum text width to be used when text wrapping is enabled (default text wrap is on)
546     /* A reasonable start for bestFitting a 90deg slice oriented on an Axis is 80% of the radius */
547     double fTextMaximumFrameWidth = 0.8 * fPieRadius;
548     const double fCompatMaxTextLen =  m_aAvailableOuterRect.getWidth() / 5.0;
549     if (m_aAvailableOuterRect.getWidth())
550     {
551         if (bHasCustomLabelPlacement)
552         {
553             // if a custom width has been provided, then use that of course,
554             // otherwise use the interoperability-compliant 1/5 of the chart space as max width
555             const awt::Size aCustomSize = rSeries.getLabelCustomSize(nPointIndex);
556             if (aCustomSize.Width > 0)
557                 fTextMaximumFrameWidth = aCustomSize.Width;
558             else
559                 fTextMaximumFrameWidth = fCompatMaxTextLen;
560         }
561         else if (nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE)
562         {
563             // use up to 80% of the available space from the slice edge to the edge of the chart
564             const sal_Int32 nOuterX = aPieLabelInfo.aOuterPosition.getX();
565             if (fAngleDegree < 90 || fAngleDegree > 270) // label is placed on the right side
566                 fTextMaximumFrameWidth = 0.8 * abs(m_aAvailableOuterRect.getWidth() - nOuterX);
567             else // label is placed on the left side
568                 fTextMaximumFrameWidth = 0.8 * nOuterX;
569 
570             // limited of course to the 1/5 maximum allowed for compatibility
571             fTextMaximumFrameWidth = std::min(fTextMaximumFrameWidth, fCompatMaxTextLen);
572          }
573     }
574     /* TODO: better guesses for INSIDE: does the slice better handle wide text or tall/wrapped text?
575      *       * wide: center near X-axis, shorter text content, slice > 90degree wide
576      *       * tall: center near Y-axis, longer text content, many categories shown
577      */
578     sal_Int32 nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth);
579 
580     ///the text shape for the label is created
581     aPieLabelInfo.xTextShape = createDataLabel(
582         xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum,
583         aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth);
584 
585     ///a new `PieLabelInfo` instance is initialized with all the info related to
586     ///the current label in order to simplify later label position rearrangement;
587     rtl::Reference< SvxShape > xChild = aPieLabelInfo.xTextShape;
588 
589     ///text shape could be empty; in that case there is no need to add label info
590     if( !xChild.is() )
591         return;
592 
593     aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get());
594 
595     if (bMovementAllowed && !m_bUseRings)
596     {
597         /** Handle the placement of the label in the best fit case.
598          *  First off the routine try to place the label inside the related pie slice,
599          *  if this is not possible the label is placed outside.
600          */
601 
602         /* Note: bestFit surprisingly does not adjust the width of the label,
603          *       so having an optimal width already set when createDataLabel ran earlier
604          *       is crucial (and currently lacking)!
605          * TODO: * change bestFit to treat the width as a max width, and reduce if beneficial
606          */
607         if (!performLabelBestFitInnerPlacement(rParam, aPieLabelInfo,
608                     fRadiusScale, aShift))
609         {
610             if (m_aAvailableOuterRect.getWidth())
611             {
612                 /* This tried to bestFit, but it didn't fit. So how best to handle this?
613                  *
614                  * Two possible cases relating to compatibility
615                  * 1.) It did fit for Microsoft, but our bestFit wasn't able to do the same
616                  *   * In that case, the best response is to be as small as possible
617                  *     (the distance from the chart edge to where the label attaches to the slice)
618                  *     to avoid scaling the diagram with too long outside labels,
619                  *     and to encourage fixing the bestFit algorithm.
620                  * 2.) It didn't fit for Microsoft either (possible, but less likely situation)
621                  *   * In that case, the compatible max length would be best
622                  *   * can expect the chart space has been properly sized to handle the max length
623                  *
624                  * In the native LO case, it is also best to be as small as possible,
625                  * so that the user creating the diagram is annoyed and makes the chart area larger.
626                  *
627                  * Therefore, handle this by making the label as small as possible.
628                  *
629                  * Complication (tdf122765.pptx): it is possible for the aOuterPosition
630                  * to be outside of the available outer rectangle (somehow),
631                  * so in that bizarre case just try the positive value of the result...
632                  */
633                 const sal_Int32 nOuterX = aPieLabelInfo.aOuterPosition.getX();
634                 if (fAngleDegree < 90 || fAngleDegree > 270) // label is placed on the right side
635                     fTextMaximumFrameWidth = 0.8 * abs(m_aAvailableOuterRect.getWidth() - nOuterX);
636                 else // label is placed on the left side
637                     fTextMaximumFrameWidth = 0.8 * nOuterX;
638 
639                 nTextMaximumFrameWidth = ceil(std::min(fTextMaximumFrameWidth, fCompatMaxTextLen));
640             }
641 
642             // find the position to connect an Outside label to
643             nScreenValueOffsetInRadiusDirection = (m_nDimension != 3) ? 150 : 0;
644             aScreenPosition2D
645                 = aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(
646                     eAlignment, css::chart::DataLabelPlacement::OUTSIDE,
647                     rParam.mfUnitCircleStartAngleDegree,
648                     rParam.mfUnitCircleWidthAngleDegree, rParam.mfUnitCircleInnerRadius,
649                     rParam.mfUnitCircleOuterRadius * fRadiusScale,
650                     rParam.mfLogicZ + 0.5, 0, aShift);
651             aPieLabelInfo.aFirstPosition
652                 = basegfx::B2IVector(aScreenPosition2D.X, aScreenPosition2D.Y);
653 
654             //add a scaling independent Offset if requested
655             if (nScreenValueOffsetInRadiusDirection != 0)
656             {
657                 basegfx::B2IVector aDirection(aScreenPosition2D.X - aOrigin.X,
658                                               aScreenPosition2D.Y - aOrigin.Y);
659                 aDirection.setLength(nScreenValueOffsetInRadiusDirection);
660                 aScreenPosition2D.X += aDirection.getX();
661                 aScreenPosition2D.Y += aDirection.getY();
662             }
663 
664             uno::Reference<drawing::XShapes> xShapes(xChild->getParent(), uno::UNO_QUERY);
665             /* question: why remove and rebuild? Can't the existing one just be changed? */
666             xShapes->remove(aPieLabelInfo.xTextShape);
667             aPieLabelInfo.xTextShape
668                 = createDataLabel(xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum,
669                                   aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth);
670             xChild = aPieLabelInfo.xTextShape;
671             if (!xChild.is())
672                 return;
673 
674             aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get());
675         }
676     }
677 
678     bool bShowLeaderLine = rSeries.getModel()
679                                    ->getFastPropertyValue(PROP_DATASERIES_SHOW_CUSTOM_LEADERLINES) // "ShowCustomLeaderLines"
680                                    .get<sal_Bool>();
681     if (m_bPieLabelsAllowToMove)
682     {
683         ::basegfx::B2IRectangle aRect(lcl_getRect(aPieLabelInfo.xLabelGroupShape));
684         sal_Int32 nPageWidth = m_aPageReferenceSize.Width;
685         sal_Int32 nPageHeight = m_aPageReferenceSize.Height;
686 
687         // the data label should be inside the chart area
688         awt::Point aShapePos = aPieLabelInfo.xLabelGroupShape->getPosition();
689         if (aRect.getMinX() < 0)
690             aPieLabelInfo.xLabelGroupShape->setPosition(
691                 awt::Point(aShapePos.X - aRect.getMinX(), aShapePos.Y));
692         if (aRect.getMinY() < 0)
693             aPieLabelInfo.xLabelGroupShape->setPosition(
694                 awt::Point(aShapePos.X, aShapePos.Y - aRect.getMinY()));
695         if (aRect.getMaxX() > nPageWidth)
696             aPieLabelInfo.xLabelGroupShape->setPosition(
697                 awt::Point(aShapePos.X - (aRect.getMaxX() - nPageWidth), aShapePos.Y));
698         if (aRect.getMaxY() > nPageHeight)
699             aPieLabelInfo.xLabelGroupShape->setPosition(
700                 awt::Point(aShapePos.X, aShapePos.Y - (aRect.getMaxY() - nPageHeight)));
701 
702         if (rSeries.isLabelCustomPos(nPointIndex) && bShowLeaderLine)
703         {
704             sal_Int32 nX1 = aPieLabelInfo.aOuterPosition.getX();
705             sal_Int32 nY1 = aPieLabelInfo.aOuterPosition.getY();
706             const sal_Int32 nX2 = std::clamp(nX1, aRect.getMinX(), aRect.getMaxX());
707             const sal_Int32 nY2 = std::clamp(nY1, aRect.getMinY(), aRect.getMaxY());
708 
709             const sal_Int32 nLabelSquaredDistanceFromOrigin
710                 = (nX2 - aOrigin.X) * (nX2 - aOrigin.X) + (nY2 - aOrigin.Y) * (nY2 - aOrigin.Y);
711             // can't use fSquaredPieRadius for 3D charts, since no longer a true circle
712             const sal_Int32 nPieEdgeSquaredDistanceFromOrigin
713                 = (nX1 - aOrigin.X) * (nX1 - aOrigin.X) + (nY1 - aOrigin.Y) * (nY1 - aOrigin.Y);
714 
715             // tdf#138018 Don't show leader line when custom positioned data label is inside pie chart
716             if (nLabelSquaredDistanceFromOrigin > nPieEdgeSquaredDistanceFromOrigin)
717             {
718                 //when the line is very short compared to the page size don't create one
719                 ::basegfx::B2DVector aLength(nX1 - nX2, nY1 - nY2);
720                 double fPageDiagonaleLength = std::hypot(nPageWidth, nPageHeight);
721                 if ((aLength.getLength() / fPageDiagonaleLength) >= 0.01)
722                 {
723                     drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } };
724 
725                     VLineProperties aVLineProperties;
726 
727                     sal_Int32 nColor = 0;
728                     nColor = rSeries.getModel()
729                                  ->getFastPropertyValue(
730                                      DataPointProperties::PROP_DATAPOINT_BORDER_COLOR)
731                                  .get<sal_Int32>();
732                     if (nColor != -1)
733                         aVLineProperties.Color <<= nColor;
734                     sal_Int32 nWidth = 0;
735                     nWidth = rSeries.getModel()
736                                  ->getFastPropertyValue(LinePropertiesHelper::PROP_LINE_WIDTH)
737                                  .get<sal_Int32>();
738                     if (nWidth != -1)
739                         aVLineProperties.Width <<= nWidth;
740 
741                     ShapeFactory::createLine2D(xTextTarget, aPoints, &aVLineProperties);
742                 }
743             }
744         }
745     }
746 
747     aPieLabelInfo.fValue = nVal;
748     aPieLabelInfo.bMovementAllowed = bMovementAllowed;
749     aPieLabelInfo.bMoved = false;
750     aPieLabelInfo.xTextTarget = xTextTarget;
751     aPieLabelInfo.bShowLeaderLine = bShowLeaderLine && !rSeries.isLabelCustomPos(nPointIndex);
752 
753     m_aLabelInfoList.push_back(aPieLabelInfo);
754 }
755 
756 // Put labels in one bar of a bar-of-pie chart. This is quite basic and doesn't
757 // deal with the possibility of the bar being too small for the label text.
createBarLabelShape(const rtl::Reference<SvxShapeGroupAnyD> & xTextTarget,VDataSeries & rSeries,sal_Int32 nPointIndex,double fBarBottom,double fBarTop,ShapeParam & rParam)758 void PieChart::createBarLabelShape(
759     const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget,
760     VDataSeries& rSeries, sal_Int32 nPointIndex, double fBarBottom,
761     double fBarTop, ShapeParam& rParam)
762 {
763     if (!rSeries.getDataPointLabelIfLabel(nPointIndex))
764         // There is no text label for this data point.  Nothing to do.
765         return;
766 
767     // Ignore the label placement specification, and just center all labels
768     const LabelAlignment eAlignment(LABEL_ALIGN_CENTER);
769 
770     css::awt::Point aPos;
771     css::awt::Size aSz;
772 
773     getBarRect(&aPos, &aSz, fBarBottom, fBarTop, rParam);
774 
775     // The screen position of the label anchor point is the center of the bar
776     awt::Point aScreenPosition2D(
777             aPos.X + aSz.Width/2.0,
778             aPos.Y + aSz.Height/2.0);
779 
780     const double fTextMaximumFrameWidth = 0.8 * (m_fBarRight - m_fBarLeft);
781     const sal_Int32 nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth);
782 
783     ///the text shape for the label is created
784     PieLabelInfo aPieLabelInfo;
785     const double nVal = rSeries.getYValue(nPointIndex);
786     aPieLabelInfo.xTextShape = createDataLabel(
787         xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum,
788         aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth);
789 
790     ///a new `PieLabelInfo` instance is initialized with all the info related to
791     ///the current label in order to simplify later label position rearrangement;
792     rtl::Reference< SvxShape > xChild = aPieLabelInfo.xTextShape;
793 
794     ///text shape could be empty; in that case there is no need to add label info
795     if( !xChild.is() )
796         return;
797 
798     aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get());
799     aPieLabelInfo.fValue = nVal;
800     aPieLabelInfo.bMovementAllowed = false;
801     aPieLabelInfo.bMoved = false;
802     aPieLabelInfo.xTextTarget = xTextTarget;
803     aPieLabelInfo.bShowLeaderLine = false;
804 
805     m_aLabelInfoList.push_back(aPieLabelInfo);
806 }
807 
addSeries(std::unique_ptr<VDataSeries> pSeries,sal_Int32,sal_Int32,sal_Int32)808 void PieChart::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 /* zSlot */, sal_Int32 /* xSlot */, sal_Int32 /* ySlot */ )
809 {
810     VSeriesPlotter::addSeries( std::move(pSeries), 0, -1, 0 );
811 }
812 
getMinimumX()813 double PieChart::getMinimumX()
814 {
815     return 0.5;
816 }
getMaxOffset()817 double PieChart::getMaxOffset()
818 {
819     if (!std::isnan(m_fMaxOffset))
820         // Value already cached.  Use it.
821         return m_fMaxOffset;
822 
823     m_fMaxOffset = 0.0;
824     if( m_aZSlots.empty() )
825         return m_fMaxOffset;
826     if( m_aZSlots.front().empty() )
827         return m_fMaxOffset;
828 
829     const std::vector< std::unique_ptr<VDataSeries> >& rSeriesList( m_aZSlots.front().front().m_aSeriesVector );
830     if(rSeriesList.empty())
831         return m_fMaxOffset;
832 
833     VDataSeries* pSeries = rSeriesList.front().get();
834     rtl::Reference< DataSeries > xSeries( pSeries->getModel() );
835     if( !xSeries.is() )
836         return m_fMaxOffset;
837 
838     double fExplodePercentage=0.0;
839     xSeries->getPropertyValue( u"Offset"_ustr) >>= fExplodePercentage;
840     if(fExplodePercentage>m_fMaxOffset)
841         m_fMaxOffset=fExplodePercentage;
842 
843     if(!m_bSizeExcludesLabelsAndExplodedSegments)
844     {
845         uno::Sequence< sal_Int32 > aAttributedDataPointIndexList;
846         // "AttributedDataPoints"
847         if( xSeries->getFastPropertyValue( PROP_DATASERIES_ATTRIBUTED_DATA_POINTS ) >>= aAttributedDataPointIndexList )
848         {
849             for(sal_Int32 nN=aAttributedDataPointIndexList.getLength();nN--;)
850             {
851                 uno::Reference< beans::XPropertySet > xPointProp( pSeries->getPropertiesOfPoint(aAttributedDataPointIndexList[nN]) );
852                 if(xPointProp.is())
853                 {
854                     fExplodePercentage=0.0;
855                     xPointProp->getPropertyValue( u"Offset"_ustr) >>= fExplodePercentage;
856                     if(fExplodePercentage>m_fMaxOffset)
857                         m_fMaxOffset=fExplodePercentage;
858                 }
859             }
860         }
861     }
862     return m_fMaxOffset;
863 }
getMaximumX()864 double PieChart::getMaximumX()
865 {
866     double fMaxOffset = getMaxOffset();
867     if( !m_aZSlots.empty() && m_bUseRings)
868         return m_aZSlots.front().size()+0.5+fMaxOffset;
869     return 1.5+fMaxOffset;
870 }
871 
getMinimumAndMaximumYInRange(double,double,sal_Int32)872 std::pair<double, double> PieChart::getMinimumAndMaximumYInRange( double /* fMinimumX */, double /* fMaximumX */, sal_Int32 /* nAxisIndex */ )
873 {
874     return { 0.0, 1.0 };
875 }
876 
isExpandBorderToIncrementRhythm(sal_Int32)877 bool PieChart::isExpandBorderToIncrementRhythm( sal_Int32 /* nDimensionIndex */ )
878 {
879     return false;
880 }
881 
isExpandIfValuesCloseToBorder(sal_Int32)882 bool PieChart::isExpandIfValuesCloseToBorder( sal_Int32 /* nDimensionIndex */ )
883 {
884     return false;
885 }
886 
isExpandWideValuesToZero(sal_Int32)887 bool PieChart::isExpandWideValuesToZero( sal_Int32 /* nDimensionIndex */ )
888 {
889     return false;
890 }
891 
isExpandNarrowValuesTowardZero(sal_Int32)892 bool PieChart::isExpandNarrowValuesTowardZero( sal_Int32 /* nDimensionIndex */ )
893 {
894     return false;
895 }
896 
isSeparateStackingForDifferentSigns(sal_Int32)897 bool PieChart::isSeparateStackingForDifferentSigns( sal_Int32 /* nDimensionIndex */ )
898 {
899     return false;
900 }
901 
902 // Determine left endpoints of connecting lines. These will terminate either
903 // at the corners of the composite wedge (if the wedge is small enough), or
904 // tangent to the left pie circle (if the wedge is larger). The endpoints
905 // are at the returned values (xl0, +/-yl0).
906 // static
leftConnEndpoints(double * xl0_p,double * yl0_p,const PieDataSrcBase * pDataSrc,const VDataSeries * pSeries,const ShapeParam & aParam)907 void PieChart::leftConnEndpoints(double* xl0_p, double* yl0_p,
908         const PieDataSrcBase *pDataSrc,
909         const VDataSeries *pSeries,
910         const ShapeParam &aParam)
911 {
912     const sal_Int32 nEnd = pDataSrc->getNPoints(pSeries, SubPieType::LEFT);
913     const double compFrac = pDataSrc->getData(pSeries, nEnd - 1,
914             SubPieType::LEFT) / aParam.mfLogicYSum;
915 
916     // Assuming temporarily that the left circle is at the origin,
917     // the tangent point (xp0, yp0) on the left circle satisfies
918     // (1) xp0 = (1-r) / t
919     // (2) xp0^2 + yp0^2 = 1
920     // where the left-hand circle has radius 1, the right-hand circle
921     // has radius r, and the right-hand circle is centered at (t, 0).
922     const double r0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale;
923     const double rho = m_fRightScale / m_fLeftScale;
924     const double xp0 = (1 - rho) / (m_fRightShift - m_fLeftShift);
925     // Determine if the composite wedge is large enough that the
926     // connecting lines hit the tangent point, instead of the corners of
927     // the wedge
928     assert(abs(xp0) <= 1.0);
929     const double theta = acos(xp0);
930 
931     double xl0, yl0;
932     if (compFrac < theta / M_PI) {
933         xl0 = r0 * cos(compFrac * M_PI);
934         yl0 = r0 * sin(compFrac * M_PI);
935     } else {
936         xl0 = r0 * xp0;
937         yl0 = sqrt(r0 * r0 - xl0 * xl0);
938     }
939     *xl0_p = xl0;
940     *yl0_p = yl0;
941 }
942 
createShapes()943 void PieChart::createShapes()
944 {
945     ///a ZSlot is a vector< vector< VDataSeriesGroup > >. There is only one
946     ///ZSlot: m_aZSlots[0] which has a number of elements equal to the total
947     ///number of data series (in fact, even if m_aZSlots[0][i] is an object of
948     ///type `VDataSeriesGroup`, in the current implementation, there is only one
949     ///data series in each data series group).
950     if (m_aZSlots.empty())
951         // No series to plot.
952         return;
953 
954     ///m_xLogicTarget is where the group of all data series shapes (e.g. a pie
955     ///slice) is added (xSeriesTarget);
956 
957     ///m_xFinalTarget is where the group of all text shapes (labels) is added
958     ///(xTextTarget).
959 
960     ///both have been already created and added to the same root shape
961     ///( a member of a VDiagram object); this initialization occurs in
962     ///`ChartView::impl_createDiagramAndContent`.
963 
964     OSL_ENSURE(m_xLogicTarget.is() && m_xFinalTarget.is(), "PieChart is not properly initialized.");
965     if (!m_xLogicTarget.is() || !m_xFinalTarget.is())
966         return;
967 
968     ///the text labels should be always on top of the other series shapes
969     ///therefore create an own group for the texts to move them to front
970     ///(because the text group is created after the series group the texts are
971     ///displayed on top)
972     rtl::Reference<SvxShapeGroupAnyD> xSeriesTarget = createGroupShape( m_xLogicTarget );
973     rtl::Reference<SvxShapeGroup> xTextTarget = ShapeFactory::createGroup2D( m_xFinalTarget );
974     //check necessary here that different Y axis can not be stacked in the same group? ... hm?
975 
976     ///pay attention that the `m_bSwapXAndY` parameter used by the polar
977     ///plotting position helper is always set to true for pie/donut charts
978     ///(see PieChart::setScales). This fact causes that `createShapes` expects
979     ///that the radius axis scale is the one with index 0 and the angle axis
980     ///scale is the one with index 1.
981 
982     std::vector< VDataSeriesGroup >::iterator             aXSlotIter = m_aZSlots.front().begin();
983     const std::vector< VDataSeriesGroup >::const_iterator aXSlotEnd = m_aZSlots.front().end();
984 
985     ///m_bUseRings == true if chart type is `donut`, == false if chart type is
986     ///`pie`; if the chart is of `donut` type we have as many rings as many data
987     ///series, else we have a single ring (a pie) representing the first data
988     ///series;
989     ///for what I can see the radius axis orientation is always reversed and
990     ///the angle axis orientation is always non-reversed;
991     ///the radius axis scale range is [0.5, number of rings + 0.5 + max_offset],
992     ///the angle axis scale range is [0, 1]. The max_offset parameter is used
993     ///for exploded pie chart and its value is 0.5.
994 
995     m_aLabelInfoList.clear();
996     m_fMaxOffset = std::numeric_limits<double>::quiet_NaN();
997     sal_Int32 n3DRelativeHeight = 100;
998     if ( (m_nDimension==3) && m_xChartTypeModel.is())
999     {
1000         try
1001         {
1002             uno::Any aAny = m_xChartTypeModel->getFastPropertyValue( PROP_PIECHARTTYPE_3DRELATIVEHEIGHT ); // "3DRelativeHeight"
1003             aAny >>= n3DRelativeHeight;
1004         }
1005         catch (const uno::Exception&) { }
1006     }
1007     ///iterate over each xslot, that is on each data series (there is
1008     ///only one data series in each data series group!); note that if the chart
1009     ///type is a pie the loop iterates only over the first data series
1010     ///(m_bUseRings||fSlotX<0.5)
1011     for( double fSlotX=0; aXSlotIter != aXSlotEnd && (m_bUseRings||fSlotX<0.5 ); ++aXSlotIter, fSlotX+=1.0 )
1012     {
1013         std::vector< std::unique_ptr<VDataSeries> >* pSeriesList = &(aXSlotIter->m_aSeriesVector);
1014         if(pSeriesList->empty())//there should be only one series in each x slot
1015             continue;
1016         VDataSeries* pSeries = pSeriesList->front().get();
1017         if(!pSeries)
1018             continue;
1019 
1020         /// The angle degree offset is set by the same property of the
1021         /// data series.
1022         /// Counter-clockwise offset from the 3 o'clock position.
1023         m_aPosHelper.m_fAngleDegreeOffset = pSeries->getStartingAngle();
1024 
1025         ///iterate through all points to get the sum of all entries of
1026         ///the current data series
1027         sal_Int32 nPointIndex=0;
1028         sal_Int32 nPointCount=pSeries->getTotalPointCount();
1029         ShapeParam aParam;
1030 
1031         for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ )
1032         {
1033             double fY = pSeries->getYValue( nPointIndex );
1034             if(fY<0.0)
1035             {
1036                 //@todo warn somehow that negative values are treated as positive
1037             }
1038             if( std::isnan(fY) )
1039                 continue;
1040             aParam.mfLogicYSum += fabs(fY);
1041         }
1042 
1043         if (aParam.mfLogicYSum == 0.0) {
1044             // Total sum of all Y values in this series is zero. Skip the whole series.
1045             continue;
1046         }
1047 
1048         PieDataSrcBase *pDataSrc = nullptr;
1049         PieDataSrc normalPieSrc;
1050         OfPieDataSrc ofPieSrc(m_nSplitPos);
1051 
1052         // Default to regular pie if too few points for of-pie
1053         ::css::chart2::PieChartSubType eSubType =
1054             nPointCount >= OfPieDataSrc::minPoints ?
1055             m_eSubType :
1056             PieChartSubType_NONE;
1057 
1058         switch (eSubType) {
1059         case PieChartSubType_NONE:
1060             pDataSrc = &normalPieSrc;
1061             createOneRing(SubPieType::NONE, fSlotX, aParam, xSeriesTarget,
1062                     xTextTarget, pSeries, pDataSrc, n3DRelativeHeight);
1063             break;
1064         case PieChartSubType_BAR:
1065         {
1066             pDataSrc = &ofPieSrc;
1067             createOneRing(SubPieType::LEFT, 0, aParam, xSeriesTarget,
1068                     xTextTarget, pSeries, pDataSrc, n3DRelativeHeight);
1069             createOneBar(SubPieType::RIGHT, aParam, xSeriesTarget,
1070                     xTextTarget, pSeries, pDataSrc, n3DRelativeHeight);
1071 
1072             //
1073             // Draw connecting lines
1074             //
1075             double xl0, xl1, yl0, yl1, x0, y0, x1, y1, y2, y3;
1076 
1077             leftConnEndpoints(&xl0, &yl0, pDataSrc, pSeries, aParam);
1078 
1079             xl0 += m_fLeftShift;
1080 
1081             // Coordinates of bar top left corner
1082             xl1 = m_fBarLeft;
1083             yl1 = m_fFullBarHeight / 2;
1084 
1085             x0 = m_aPosHelper.transformUnitCircleToScene(0, xl0, 0).PositionX;
1086             y0 = m_aPosHelper.transformUnitCircleToScene(90, yl0, 0).PositionY;
1087             x1 = m_aPosHelper.transformUnitCircleToScene(0, xl1, 0).PositionX;
1088             y1 = m_aPosHelper.transformUnitCircleToScene(90, yl1, 0).PositionY;
1089             y2 = m_aPosHelper.transformUnitCircleToScene(90, -yl0, 0).PositionY;
1090             y3 = m_aPosHelper.transformUnitCircleToScene(90, -yl1, 0).PositionY;
1091 
1092             std::vector<std::vector<css::drawing::Position3D>> linePts;
1093             linePts.resize(2);
1094             linePts[0].push_back(css::drawing::Position3D(x0, y0, aParam.mfLogicZ));
1095             linePts[0].push_back(css::drawing::Position3D(x1, y1, aParam.mfLogicZ));
1096             linePts[1].push_back(css::drawing::Position3D(x0, y2, aParam.mfLogicZ));
1097             linePts[1].push_back(css::drawing::Position3D(x1, y3, aParam.mfLogicZ));
1098 
1099             VLineProperties aVLineProperties;   // default black
1100 
1101             //create line
1102             rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes =
1103                 getSeriesGroupShape(pSeries, xSeriesTarget);
1104             rtl::Reference<SvxShape> xShape = ShapeFactory::createLine2D(
1105                     xSeriesGroupShape_Shapes, linePts, &aVLineProperties);
1106 
1107             // need to set properties?
1108             //PropertyMapper::setMappedProperties( *xShape, xObjectProperties,
1109             //        PropertyMapper::getPropertyNameMapForLineSeriesProperties() );
1110 
1111             break;
1112         }
1113         case PieChartSubType_PIE:
1114         {
1115             pDataSrc = &ofPieSrc;
1116             createOneRing(SubPieType::LEFT, 0, aParam, xSeriesTarget,
1117                     xTextTarget, pSeries, pDataSrc, n3DRelativeHeight);
1118             createOneRing(SubPieType::RIGHT, 0, aParam, xSeriesTarget,
1119                     xTextTarget, pSeries, pDataSrc, n3DRelativeHeight);
1120 
1121             //
1122             // Draw connecting lines
1123             //
1124             double xl0, xl1, yl0, yl1, x0, y0, x1, y1, y2, y3;
1125 
1126             leftConnEndpoints(&xl0, &yl0, pDataSrc, pSeries, aParam);
1127 
1128             // Translated, per below
1129             xl0 += m_fLeftShift - m_fRightShift;
1130 
1131             // Compute tangent point on the right-hand circle of the line
1132             // through (xl0, yl0). If we translate things so the right-hand
1133             // circle is centered on the origin, then this point (x,y)
1134             // satisfies these two equations, where r1 is the radius of the
1135             // right-hand circle:
1136             // (1) x^2 + y^2 = r1^2
1137             // (2) (y - yl0) / (x - xl0) = -x / y
1138             const double r1 = aParam.mfUnitCircleOuterRadius * m_fRightScale;
1139             xl1 = (r1*r1 * xl0 + yl0 * r1 * sqrt(xl0*xl0 + yl0*yl0 - r1*r1)) /
1140                 (xl0*xl0 + yl0*yl0);
1141             yl1 = sqrt(r1*r1 - xl1*xl1);
1142 
1143             // Now translate back to the coordinates we use
1144             xl0 += m_fRightShift;
1145             xl1 += m_fRightShift;
1146 
1147             x0 = m_aPosHelper.transformUnitCircleToScene(0, xl0, 0).PositionX;
1148             y0 = m_aPosHelper.transformUnitCircleToScene(90, yl0, 0).PositionY;
1149             x1 = m_aPosHelper.transformUnitCircleToScene(0, xl1, 0).PositionX;
1150             y1 = m_aPosHelper.transformUnitCircleToScene(90, yl1, 0).PositionY;
1151             y2 = m_aPosHelper.transformUnitCircleToScene(90, -yl0, 0).PositionY;
1152             y3 = m_aPosHelper.transformUnitCircleToScene(90, -yl1, 0).PositionY;
1153 
1154             std::vector<std::vector<css::drawing::Position3D>> linePts;
1155             linePts.resize(2);
1156             linePts[0].push_back(css::drawing::Position3D(x0, y0, aParam.mfLogicZ));
1157             linePts[0].push_back(css::drawing::Position3D(x1, y1, aParam.mfLogicZ));
1158             linePts[1].push_back(css::drawing::Position3D(x0, y2, aParam.mfLogicZ));
1159             linePts[1].push_back(css::drawing::Position3D(x1, y3, aParam.mfLogicZ));
1160 
1161             VLineProperties aVLineProperties;   // default black
1162 
1163             //create line
1164             rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes =
1165                 getSeriesGroupShape(pSeries, xSeriesTarget);
1166             rtl::Reference<SvxShape> xShape = ShapeFactory::createLine2D(
1167                     xSeriesGroupShape_Shapes, linePts, &aVLineProperties);
1168 
1169             break;
1170         }
1171         default:
1172             assert(false); // this shouldn't happen
1173         }
1174     }//next x slot
1175 }
1176 
propIndex(sal_Int32 nPointIndex,enum SubPieType eType,const PieDataSrcBase * pDataSrc,const VDataSeries * pSeries)1177 static sal_Int32 propIndex(
1178         sal_Int32 nPointIndex,
1179         enum SubPieType eType,
1180         const PieDataSrcBase *pDataSrc,
1181         const VDataSeries* pSeries)
1182 {
1183 
1184     switch (eType) {
1185     case SubPieType::LEFT:
1186         if (nPointIndex == pDataSrc->getNPoints(pSeries,
1187                     SubPieType::LEFT) - 1) {
1188             return pSeries->getTotalPointCount();
1189         } else {
1190             return nPointIndex;
1191         }
1192         break;
1193     case SubPieType::RIGHT:
1194         return pDataSrc->getNPoints(pSeries, SubPieType::LEFT) +
1195             nPointIndex - 1;
1196         break;
1197     case SubPieType::NONE:
1198         return nPointIndex;
1199         break;
1200     default: // shouldn't happen
1201         assert(false);
1202         return 0; // suppress compile warning
1203     }
1204 }
1205 
1206 
createOneRing(enum SubPieType eType,double fSlotX,ShapeParam & aParam,const rtl::Reference<SvxShapeGroupAnyD> & xSeriesTarget,const rtl::Reference<SvxShapeGroup> & xTextTarget,VDataSeries * pSeries,const PieDataSrcBase * pDataSrc,sal_Int32 n3DRelativeHeight)1207 void PieChart::createOneRing(
1208         enum SubPieType eType,
1209         double fSlotX,
1210         ShapeParam& aParam,
1211         const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget,
1212         const rtl::Reference<SvxShapeGroup>& xTextTarget,
1213         VDataSeries* pSeries,
1214         const PieDataSrcBase *pDataSrc,
1215         sal_Int32 n3DRelativeHeight)
1216 {
1217     bool bHasFillColorMapping = pSeries->hasPropertyMapping(u"FillColor"_ustr);
1218 
1219     sal_Int32 nRingPtCnt = pDataSrc->getNPoints(pSeries, eType);
1220 
1221     // Find sum of entries for this ring or sub-pie
1222     double ringSum = 0;
1223     for (sal_Int32 nPointIndex = 0; nPointIndex < nRingPtCnt; nPointIndex++ ) {
1224         double fY = pDataSrc->getData(pSeries, nPointIndex, eType);
1225         if (!std::isnan(fY) ) ringSum += fY;
1226     }
1227 
1228     // determine the starting angle around the ring
1229     auto sAngle = [&]()
1230     {
1231         if (eType == SubPieType::LEFT) {
1232             // Left of-pie has the "composite" wedge (the one expanded in the right
1233             // subgraph) facing to the right in the chart, to allow the expansion
1234             // lines to meet it
1235             const double compositeVal = pDataSrc->getData(pSeries, nRingPtCnt - 1, eType);
1236             const double degAng = compositeVal * 360 / (ringSum * 2);
1237             return m_aPosHelper.clockwiseWedges() ? 360 - degAng : degAng;
1238         } else {
1239             /// The angle degree offset is set by the same property of the
1240             /// data series.
1241             /// Counter-clockwise offset from the 3 o'clock position.
1242             return static_cast<double>(pSeries->getStartingAngle());
1243         }
1244     };
1245 
1246     m_aPosHelper.m_fAngleDegreeOffset = sAngle();
1247 
1248     ///the `explodeable` ring is the first one except when the radius axis
1249     ///orientation is reversed (always!?) and we are dealing with a donut: in
1250     ///such a case the `explodeable` ring is the last one.
1251     std::vector< VDataSeriesGroup >::size_type nExplodeableSlot = 0;
1252     if( m_aPosHelper.isMathematicalOrientationRadius() && m_bUseRings )
1253         nExplodeableSlot = m_aZSlots.front().size()-1;
1254 
1255     double fLogicYForNextPoint = 0.0;
1256     ///iterate through all points to create shapes
1257     for(sal_Int32 nPointIndex = 0; nPointIndex < nRingPtCnt; nPointIndex++ )
1258     {
1259         double fLogicInnerRadius, fLogicOuterRadius;
1260 
1261         ///compute the maximum relative distance offset of the current slice
1262         ///from the pie center
1263         ///it is worth noting that after the first invocation the maximum
1264         ///offset value is cached, so it is evaluated only once per each
1265         ///call to `createShapes`
1266         double fOffset = getMaxOffset();
1267 
1268         ///compute the outer and the inner radius for the current ring slice
1269         bool bIsVisible = m_aPosHelper.getInnerAndOuterRadius( fSlotX+1.0, fLogicInnerRadius, fLogicOuterRadius, m_bUseRings, fOffset );
1270         if( !bIsVisible )
1271             continue;
1272 
1273         aParam.mfDepth  = getTransformedDepth() * (n3DRelativeHeight / 100.0);
1274 
1275         rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries, xSeriesTarget);
1276 
1277         ///collect data point information (logic coordinates, style ):
1278         double fLogicYValue = pDataSrc->getData(pSeries, nPointIndex, eType);
1279         if( std::isnan(fLogicYValue) )
1280             continue;
1281         if(fLogicYValue==0.0)//@todo: continue also if the resolution is too small
1282             continue;
1283         double fLogicYPos = fLogicYForNextPoint;
1284         fLogicYForNextPoint += fLogicYValue;
1285 
1286         uno::Reference< beans::XPropertySet > xPointProperties =
1287             pDataSrc->getProps(pSeries, nPointIndex, eType);
1288 
1289         //iterate through all subsystems to create partial points
1290         {
1291             //logic values on angle axis:
1292             double fLogicStartAngleValue = fLogicYPos / ringSum;
1293             double fLogicEndAngleValue = (fLogicYPos+fLogicYValue) / ringSum;
1294 
1295             ///note that the explode percentage is set to the `Offset`
1296             ///property of the current data series entry only for slices
1297             ///belonging to the outer ring
1298             aParam.mfExplodePercentage = 0.0;
1299             bool bDoExplode = ( nExplodeableSlot == static_cast< std::vector< VDataSeriesGroup >::size_type >(fSlotX) );
1300             if(bDoExplode) try
1301             {
1302                 xPointProperties->getPropertyValue( u"Offset"_ustr) >>= aParam.mfExplodePercentage;
1303             }
1304             catch( const uno::Exception& )
1305             {
1306                 TOOLS_WARN_EXCEPTION("chart2", "" );
1307             }
1308 
1309             ///see notes for `PolarPlottingPositionHelper` methods
1310             ///transform to unit circle:
1311             aParam.mfUnitCircleWidthAngleDegree = m_aPosHelper.getWidthAngleDegree( fLogicStartAngleValue, fLogicEndAngleValue );
1312             aParam.mfUnitCircleStartAngleDegree = m_aPosHelper.transformToAngleDegree( fLogicStartAngleValue );
1313             aParam.mfUnitCircleInnerRadius = m_aPosHelper.transformToRadius( fLogicInnerRadius );
1314             aParam.mfUnitCircleOuterRadius = m_aPosHelper.transformToRadius( fLogicOuterRadius );
1315 
1316             ///create data point
1317             aParam.mfLogicZ = -1.0; // For 3D pie chart label position
1318 
1319             // Do concentric explosion if it's a donut chart with more than one series
1320             const bool bConcentricExplosion = m_bUseRings && (m_aZSlots.front().size() > 1);
1321             rtl::Reference<SvxShape> xPointShape =
1322                 createDataPoint(eType, xSeriesGroupShape_Shapes,
1323                         xPointProperties, aParam, nRingPtCnt,
1324                         bConcentricExplosion);
1325 
1326             // Handle coloring of the composite wedge
1327             sal_Int32 nPropIdx = propIndex(nPointIndex, eType, pDataSrc,
1328                     pSeries);
1329 
1330             ///point color:
1331             if (!pSeries->hasPointOwnColor(nPropIdx) && m_xColorScheme.is())
1332             {
1333                 xPointShape->setPropertyValue(u"FillColor"_ustr,
1334                     uno::Any(m_xColorScheme->getColorByIndex( nPropIdx )));
1335             }
1336 
1337 
1338             if(bHasFillColorMapping)
1339             {
1340                 double nPropVal = pSeries->getValueByProperty(nPropIdx, u"FillColor"_ustr);
1341                 if(!std::isnan(nPropVal))
1342                 {
1343                     xPointShape->setPropertyValue(u"FillColor"_ustr, uno::Any(static_cast<sal_Int32>( nPropVal)));
1344                 }
1345             }
1346 
1347             ///create label, *except* for composite wedge
1348             if (!(eType == SubPieType::LEFT && nPointIndex == pDataSrc->getNPoints(pSeries,
1349                     SubPieType::LEFT) - 1)) {
1350                 createTextLabelShape(xTextTarget, *pSeries, nPropIdx, aParam, eType);
1351             }
1352 
1353             if(!bDoExplode)
1354             {
1355                 ShapeFactory::setShapeName( xPointShape
1356                             , ObjectIdentifier::createPointCID(
1357                                 pSeries->getPointCID_Stub(), nPropIdx ) );
1358             }
1359             else try
1360             {
1361                 ///enable dragging of outer segments
1362 
1363                 double fAngle  = aParam.mfUnitCircleStartAngleDegree + aParam.mfUnitCircleWidthAngleDegree/2.0;
1364                 double fMaxDeltaRadius = aParam.mfUnitCircleOuterRadius-aParam.mfUnitCircleInnerRadius;
1365                 drawing::Position3D aOrigin = m_aPosHelper.transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius, aParam.mfLogicZ );
1366                 drawing::Position3D aNewOrigin = m_aPosHelper.transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius + fMaxDeltaRadius, aParam.mfLogicZ );
1367 
1368                 sal_Int32 nOffsetPercent( static_cast<sal_Int32>(aParam.mfExplodePercentage * 100.0) );
1369 
1370                 awt::Point aMinimumPosition( PlottingPositionHelper::transformSceneToScreenPosition(
1371                     aOrigin, m_xLogicTarget, m_nDimension ) );
1372                 awt::Point aMaximumPosition( PlottingPositionHelper::transformSceneToScreenPosition(
1373                     aNewOrigin, m_xLogicTarget, m_nDimension ) );
1374 
1375                 //enable dragging of piesegments
1376                 OUString aPointCIDStub( ObjectIdentifier::createSeriesSubObjectStub( OBJECTTYPE_DATA_POINT
1377                     , pSeries->getSeriesParticle()
1378                     , ObjectIdentifier::getPieSegmentDragMethodServiceName()
1379                     , ObjectIdentifier::createPieSegmentDragParameterString(
1380                         nOffsetPercent, aMinimumPosition, aMaximumPosition )
1381                     ) );
1382 
1383                 ShapeFactory::setShapeName( xPointShape
1384                             , ObjectIdentifier::createPointCID( aPointCIDStub,
1385                                 nPropIdx ) );
1386             }
1387             catch( const uno::Exception& )
1388             {
1389                 TOOLS_WARN_EXCEPTION("chart2", "" );
1390             }
1391         }//next series in x slot (next y slot)
1392     }//next category
1393 }
1394 
createOneBar(enum SubPieType eType,ShapeParam & aParam,const rtl::Reference<SvxShapeGroupAnyD> & xSeriesTarget,const rtl::Reference<SvxShapeGroup> & xTextTarget,VDataSeries * pSeries,const PieDataSrcBase * pDataSrc,sal_Int32 n3DRelativeHeight)1395 void PieChart::createOneBar(
1396         enum SubPieType eType,
1397         ShapeParam& aParam,
1398         const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget,
1399         const rtl::Reference<SvxShapeGroup>& xTextTarget,
1400         VDataSeries* pSeries,
1401         const PieDataSrcBase *pDataSrc,
1402         sal_Int32 n3DRelativeHeight)
1403 {
1404     bool bHasFillColorMapping = pSeries->hasPropertyMapping(u"FillColor"_ustr);
1405 
1406     sal_Int32 nBarPtCnt = pDataSrc->getNPoints(pSeries, eType);
1407 
1408     // Find sum of entries for this bar chart
1409     double barSum = 0;
1410     for (sal_Int32 nPointIndex = 0; nPointIndex < nBarPtCnt; nPointIndex++ ) {
1411         double fY = pDataSrc->getData(pSeries, nPointIndex, eType);
1412         if (!std::isnan(fY) ) barSum += fY;
1413     }
1414 
1415     double fBarBottom = 0.0;
1416     double fBarTop = -0.5;  // make the bar go from -0.5 to 0.5
1417     ///iterate through all points to create shapes
1418     for(sal_Int32 nPointIndex = 0; nPointIndex < nBarPtCnt; nPointIndex++ )
1419     {
1420         aParam.mfDepth  = getTransformedDepth() * (n3DRelativeHeight / 100.0);
1421 
1422         rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries, xSeriesTarget);
1423 
1424         ///collect data point information (logic coordinates, style ):
1425         double fY = o3tl::div_allow_zero(pDataSrc->getData(pSeries, nPointIndex, eType), barSum);
1426         if( std::isnan(fY) )
1427             continue;
1428         if(fY==0.0)//@todo: continue also if the resolution is too small
1429             continue;
1430         fBarBottom = fBarTop;
1431         fBarTop += fY;
1432 
1433         uno::Reference< beans::XPropertySet > xPointProperties =
1434             pDataSrc->getProps(pSeries, nPointIndex, eType);
1435 
1436         ///create data point
1437         aParam.mfLogicZ = -1.0; // For 3D pie chart label position
1438 
1439         rtl::Reference<SvxShape> xPointShape =
1440             createBarDataPoint(xSeriesGroupShape_Shapes,
1441                     xPointProperties, aParam,
1442                     fBarBottom, fBarTop);
1443 
1444         sal_Int32 nPropIdx = propIndex(nPointIndex, eType, pDataSrc, pSeries);
1445 
1446         ///point color:
1447         if (!pSeries->hasPointOwnColor(nPropIdx) && m_xColorScheme.is())
1448         {
1449             xPointShape->setPropertyValue(u"FillColor"_ustr,
1450                 uno::Any(m_xColorScheme->getColorByIndex( nPropIdx )));
1451         }
1452 
1453 
1454         if(bHasFillColorMapping)
1455         {
1456             double nPropVal = pSeries->getValueByProperty(nPropIdx, u"FillColor"_ustr);
1457             if(!std::isnan(nPropVal))
1458             {
1459                 xPointShape->setPropertyValue(u"FillColor"_ustr, uno::Any(static_cast<sal_Int32>( nPropVal)));
1460             }
1461         }
1462 
1463         ///create label
1464         createBarLabelShape(xTextTarget, *pSeries, nPropIdx, fBarBottom,
1465                 fBarTop, aParam);
1466 
1467         ShapeFactory::setShapeName( xPointShape,
1468                 ObjectIdentifier::createPointCID( pSeries->getPointCID_Stub(),
1469                     nPropIdx ) );
1470     }//next category
1471 }
1472 
PieLabelInfo()1473 PieChart::PieLabelInfo::PieLabelInfo()
1474     :  fValue(0.0)
1475     , bMovementAllowed(false), bMoved(false)
1476     , bShowLeaderLine(false), pPrevious(nullptr)
1477     , pNext(nullptr)
1478 {
1479 }
1480 
1481 /** In case this label and the passed label overlap the routine moves this
1482  *  label in order to fix the issue. After the label position has been
1483  *  rearranged it is checked that the moved label is still inside the page
1484  *  document, if the test is positive the routine returns true else returns
1485  *  false.
1486  */
moveAwayFrom(const PieChart::PieLabelInfo * pFix,const awt::Size & rPageSize,bool bMoveHalfWay,bool bMoveClockwise)1487 bool PieChart::PieLabelInfo::moveAwayFrom( const PieChart::PieLabelInfo* pFix, const awt::Size& rPageSize, bool bMoveHalfWay, bool bMoveClockwise )
1488 {
1489     //return true if the move was successful
1490     if(!bMovementAllowed)
1491         return false;
1492 
1493     const sal_Int32 nLabelDistanceX = rPageSize.Width/50;
1494     const sal_Int32 nLabelDistanceY = rPageSize.Height/50;
1495 
1496     ///compute the rectangle representing the intersection of the label bounding
1497     ///boxes (`aOverlap`).
1498     ::basegfx::B2IRectangle aOverlap( lcl_getRect( xLabelGroupShape ) );
1499     aOverlap.intersect( lcl_getRect( pFix->xLabelGroupShape ) );
1500     if( aOverlap.isEmpty() )
1501         return true;
1502 
1503     //TODO: alternative move direction
1504 
1505     ///the label is shifted along the direction orthogonal to the vector
1506     ///starting at the pie/donut center and ending at this label anchor
1507     ///point;
1508 
1509     ///named `aTangentialDirection` the unit vector related to such a
1510     ///direction, the magnitude of the shift along such a direction is
1511     ///calculated in this way: if the horizontal component of
1512     ///`aTangentialDirection` is greater than the vertical component,
1513     ///the magnitude of the shift is equal to `aOverlap.Width` else to
1514     ///`aOverlap.Height`;
1515     basegfx::B2IVector aRadiusDirection = aFirstPosition - aOrigin;
1516     aRadiusDirection.setLength(1.0);
1517     basegfx::B2IVector aTangentialDirection( -aRadiusDirection.getY(), aRadiusDirection.getX() );
1518     bool bShiftHorizontal = abs(aTangentialDirection.getX()) > abs(aTangentialDirection.getY());
1519     sal_Int32 nShift = bShiftHorizontal ? static_cast<sal_Int32>(aOverlap.getWidth()) : static_cast<sal_Int32>(aOverlap.getHeight());
1520     ///the magnitude of the shift is also increased by 1/50-th of the width
1521     ///or the height of the document page;
1522     nShift += (bShiftHorizontal ? nLabelDistanceX : nLabelDistanceY);
1523     ///in case the `bMoveHalfWay` parameter is true the magnitude of
1524     ///the shift is halved.
1525     if( bMoveHalfWay )
1526         nShift/=2;
1527     ///in case the `bMoveClockwise` parameter is false the direction of
1528     ///`aTangentialDirection` is reversed;
1529     if(!bMoveClockwise)
1530         nShift*=-1;
1531     awt::Point aOldPos( xLabelGroupShape->getPosition() );
1532     basegfx::B2IVector aNewPos = basegfx::B2IVector( aOldPos.X, aOldPos.Y ) + nShift*aTangentialDirection;
1533 
1534     ///a final check is performed in order to be sure that the moved label
1535     ///is still inside the page document;
1536     awt::Point aNewAWTPos( aNewPos.getX(), aNewPos.getY() );
1537     if( !lcl_isInsidePage( aNewAWTPos, xLabelGroupShape->getSize(), rPageSize ) )
1538         return false;
1539 
1540     xLabelGroupShape->setPosition( aNewAWTPos );
1541     bMoved = true;
1542 
1543     return true;
1544 
1545     ///note that no further test is performed in order to check that the
1546     ///overlap is really fixed: this result is surely achieved if the shift
1547     ///would occur in the horizontal or vertical direction (since, in such a
1548     ///direction, the magnitude of the shift would be greater than the length
1549     ///of the overlap), but in general this is not true;
1550     ///adding a constant term equal to 1/50-th of the width or the height of
1551     ///the document page increases the probability of success, anyway it is
1552     ///worth noting that the method can return true even if the overlap issue
1553     ///is not (completely) fixed;
1554 }
1555 
resetLabelPositionsToPreviousState()1556 void PieChart::resetLabelPositionsToPreviousState()
1557 {
1558     for (auto const& labelInfo : m_aLabelInfoList)
1559         labelInfo.xLabelGroupShape->setPosition(labelInfo.aPreviousPosition);
1560 }
1561 
detectLabelOverlapsAndMove(const awt::Size & rPageSize)1562 bool PieChart::detectLabelOverlapsAndMove( const awt::Size& rPageSize )
1563 {
1564     ///the routine tries to individuate a chain of overlapping labels and
1565     ///assigns the first and the last of them to `pFirstBorder` and
1566     ///`pSecondBorder`;
1567     ///this result is achieved by performing two consecutive while loop.
1568 
1569     ///find borders of a group of overlapping labels
1570 
1571     ///a first while loop is started on the collection of `PieLabelInfo` objects;
1572     ///the bounding box of each label is checked for overlap against the bounding
1573     ///box of the previous and of the next label;
1574     ///when an overlap is found `bOverlapFound` is set to true, however the
1575     ///iteration is break only if the overlap occurs against only the next label
1576     ///and not against the previous label: so we exit from the loop whenever an
1577     ///overlap occurs except when the loop initial label overlaps with the
1578     ///previous one;
1579     bool bOverlapFound = false;
1580     PieLabelInfo* pStart = &(*(m_aLabelInfoList.rbegin()));
1581     PieLabelInfo* pFirstBorder = nullptr;
1582     PieLabelInfo* pSecondBorder = nullptr;
1583     PieLabelInfo* pCurrent = pStart;
1584     do
1585     {
1586         ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) );
1587         ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap );
1588         aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) );
1589         aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) );
1590 
1591         bool bPreviousOverlap = !aPreviousOverlap.isEmpty();
1592         bool bNextOverlap = !aNextOverlap.isEmpty();
1593         if( bPreviousOverlap || bNextOverlap )
1594             bOverlapFound = true;
1595         if( !bPreviousOverlap && bNextOverlap )
1596         {
1597             pFirstBorder = pCurrent;
1598             break;
1599         }
1600         pCurrent = pCurrent->pNext;
1601     }
1602     while( pCurrent != pStart );
1603 
1604     if( !bOverlapFound )
1605         return false;
1606 
1607     ///in case we found a label (`pFirstBorder`) which overlaps with the next
1608     ///label and not with the previous label a second while loop is started with
1609     ///`pFirstBorder` as initial label; one more time the bounding box of each
1610     ///label is checked for overlap against the bounding box of the previous and
1611     ///of the next label, however this time we exit from the loop only if the
1612     ///current label overlaps with the previous one but does not with the next
1613     ///one (the opposite of what is required in the former loop);
1614     ///in case such a label is found it is assigned to `pSecondBorder` and the
1615     ///iteration is stopped; so in case there is a chain of overlapping labels
1616     ///we end up having the first label of the chain pointed by `pFirstBorder`
1617     ///and the last label of the chain pointed by `pSecondBorder`;
1618     if( pFirstBorder )
1619     {
1620         pCurrent = pFirstBorder;
1621         do
1622         {
1623             ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) );
1624             ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap );
1625             aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) );
1626             aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) );
1627 
1628             if( !aPreviousOverlap.isEmpty() && aNextOverlap.isEmpty() )
1629             {
1630                 pSecondBorder = pCurrent;
1631                 break;
1632             }
1633             pCurrent = pCurrent->pNext;
1634         }
1635         while( pCurrent != pFirstBorder );
1636     }
1637 
1638     ///when two labels satisfying the required conditions are not found
1639     ///(`pFirstBorder == 0 || pSecondBorder == 0`) but still an overlap occurs
1640     ///(`bOverlapFound == true`) we are in the situation where each label
1641     ///overlaps with both the previous and the next one; so `pFirstBorder` is
1642     ///set to point to the last `PieLabelInfo` object in the collection and
1643     ///`pSecondBorder` is set to point to the first one;
1644     if( !pFirstBorder || !pSecondBorder )
1645     {
1646         pFirstBorder = &(*(m_aLabelInfoList.rbegin()));
1647         pSecondBorder = &(*(m_aLabelInfoList.begin()));
1648     }
1649 
1650     ///the total number of labels that made up the chain is calculated and used
1651     ///for getting a pointer to the central label (`pCenter`);
1652     PieLabelInfo* pCenter = pFirstBorder;
1653     sal_Int32 nOverlapGroupCount = 1;
1654     for( pCurrent = pFirstBorder ;pCurrent != pSecondBorder; pCurrent = pCurrent->pNext )
1655         nOverlapGroupCount++;
1656     sal_Int32 nCenterPos = nOverlapGroupCount/2;
1657     bool bSingleCenter = nOverlapGroupCount%2 != 0;
1658     if( bSingleCenter )
1659         nCenterPos++;
1660     if(nCenterPos>1)
1661     {
1662         pCurrent = pFirstBorder;
1663         while( --nCenterPos )
1664             pCurrent = pCurrent->pNext;
1665         pCenter = pCurrent;
1666     }
1667 
1668     ///the current position of each label in the collection is saved in
1669     ///`PieLabelInfo.aPreviousPosition`, so that it is possible to undo the label
1670     ///move action if it is needed; the undo action is provided by the
1671     ///`PieChart::resetLabelPositionsToPreviousState` method.
1672     pCurrent = pStart;
1673     do
1674     {
1675         pCurrent->aPreviousPosition = pCurrent->xLabelGroupShape->getPosition();
1676         pCurrent = pCurrent->pNext;
1677     }
1678     while( pCurrent != pStart );
1679 
1680     ///the `PieChart::tryMoveLabels` method is invoked with
1681     ///`rbAlternativeMoveDirection` boolean parameter set to false, such a method
1682     ///tries to remove all overlaps that occur in the list of labels going from
1683     ///`pFirstBorder` to `pSecondBorder`;
1684     ///if the `PieChart::tryMoveLabels` returns true no further action is
1685     ///performed, however it is worth noting that it does not mean that all
1686     ///overlap issues have been surely fixed, but only that all moved labels are
1687     ///at least completely inside the page document;
1688     ///when `PieChart::tryMoveLabels` returns false, it means that the attempt
1689     ///to fix one of the overlap issues caused that a label has been moved
1690     ///(partially) outside the page document (anyway the `PieChart::tryMoveLabels`
1691     ///method takes care to restore the position of all labels to their initial
1692     ///position, and to set the `rbAlternativeMoveDirection` in/out parameter to
1693     ///true); in such a case a second invocation of `PieChart::tryMoveLabels` is
1694     ///performed (and this time the `rbAlternativeMoveDirection` boolean
1695     ///parameter is true) and independently by what the `PieChart::tryMoveLabels`
1696     ///method returns no further action is performed;
1697     ///(see notes for `PieChart::tryMoveLabels`);
1698     bool bAlternativeMoveDirection = false;
1699     if( !tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize ) )
1700         tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize );
1701 
1702     ///in both cases (one or two invocations of `PieChart::tryMoveLabels`) the
1703     ///`detectLabelOverlapsAndMove` method ends returning true.
1704     return true;
1705 }
1706 
1707 
1708 /** Try to remove all overlaps that occur in the list of labels going from
1709  *  `pFirstBorder` to `pSecondBorder`
1710  */
tryMoveLabels(PieLabelInfo const * pFirstBorder,PieLabelInfo const * pSecondBorder,PieLabelInfo * pCenter,bool bSingleCenter,bool & rbAlternativeMoveDirection,const awt::Size & rPageSize)1711 bool PieChart::tryMoveLabels( PieLabelInfo const * pFirstBorder, PieLabelInfo const * pSecondBorder
1712                              , PieLabelInfo* pCenter
1713                              , bool bSingleCenter, bool& rbAlternativeMoveDirection, const awt::Size& rPageSize )
1714 {
1715 
1716     PieLabelInfo* p1 = bSingleCenter ? pCenter->pPrevious : pCenter;
1717     PieLabelInfo* p2 = pCenter->pNext;
1718     //return true when successful
1719 
1720     bool bLabelOrderIsAntiClockWise = m_aPosHelper.isMathematicalOrientationAngle();
1721 
1722     ///two loops are performed simultaneously: the outer loop iterates on
1723     ///`PieLabelInfo` objects in the list starting from the central element
1724     ///(`pCenter`) and moving forward until the last element (`pSecondBorder`);
1725     ///the inner loop starts from the previous element of `pCenter` and moves
1726     ///forward until the current `PieLabelInfo` object of the outer loop is
1727     ///reached
1728     PieLabelInfo* pCurrent = nullptr;
1729     for( pCurrent = p2 ;pCurrent->pPrevious != pSecondBorder; pCurrent = pCurrent->pNext )
1730     {
1731         PieLabelInfo* pFix = nullptr;
1732         for( pFix = p2->pPrevious ;pFix != pCurrent; pFix = pFix->pNext )
1733         {
1734             ///on the current `PieLabelInfo` object of the outer loop the
1735             ///`moveAwayFrom` method is invoked by passing the current
1736             ///`PieLabelInfo` object of the inner loop as argument.
1737 
1738             ///so each label going from the central one to the last one is
1739             ///checked for overlapping against all previous labels (that comes
1740             ///after the central label) and in case the overlap occurs the
1741             ///`moveAwayFrom` method tries to fix the issue;
1742             ///if `moveAwayFrom` returns true (pay attention: that does not
1743             ///mean that the overlap issue has been surely fixed but only that
1744             ///the moved label is at least completely inside the page document:
1745             ///see notes on `PieChart::PieLabelInfo::moveAwayFrom`), the inner
1746             ///loop starts a new iteration else the `rbAlternativeMoveDirection`
1747             ///boolean parameter is tested: if it is false the parameter is set
1748             ///to true, the position of all labels is restored to the initial
1749             ///one (through the `PieChart::resetLabelPositionsToPreviousState`
1750             ///method) and the method ends by returning false, else the inner
1751             ///loop starts a new iteration step;
1752             ///so when `rbAlternativeMoveDirection` is true the method goes on
1753             ///trying to fix left overlap issues even if the last `moveAwayFrom`
1754             ///invocation has moved a label in a position that it is not
1755             ///completely inside the page document
1756 
1757             if( !pCurrent->moveAwayFrom( pFix, rPageSize, !bSingleCenter && pCurrent == p2, !bLabelOrderIsAntiClockWise ) )
1758             {
1759                 if( !rbAlternativeMoveDirection )
1760                 {
1761                     rbAlternativeMoveDirection = true;
1762                     resetLabelPositionsToPreviousState();
1763                     return false;
1764                 }
1765             }
1766         }
1767     }
1768 
1769     ///if the method does not return before ending the first pair of loops,
1770     ///a second pair of simultaneous loops is performed in the opposite
1771     ///direction (respect with the previous case): the outer loop iterates on
1772     ///`PieLabelInfo` objects in the list starting from the central element
1773     ///(`pCenter`) and moving backward until the first element (`pFirstBorder`);
1774     ///the inner loop starts from the next element of `pCenter` and moves
1775     ///backward until the current `PieLabelInfo` object of the outer loop is
1776     ///reached
1777 
1778     ///like in the previous case on the current `PieLabelInfo` object of
1779     ///the outer loop the `moveAwayFrom` method is invoked by passing
1780     ///the current `PieLabelInfo` object of the inner loop as argument
1781 
1782     ///so each label going from the central one to the first one is checked for
1783     ///overlapping on all subsequent labels (that come before the central label)
1784     ///and in case the overlap occurs the `moveAwayFrom` method tries to fix
1785     ///the issue. The subsequent actions performed after the invocation
1786     ///`moveAwayFrom` are the same detailed above for the first pair of loops
1787 
1788     for( pCurrent = p1 ;pCurrent->pNext != pFirstBorder; pCurrent = pCurrent->pPrevious )
1789     {
1790         PieLabelInfo* pFix = nullptr;
1791         for( pFix = p2->pNext ;pFix != pCurrent; pFix = pFix->pPrevious )
1792         {
1793             if( !pCurrent->moveAwayFrom( pFix, rPageSize, false, bLabelOrderIsAntiClockWise ) )
1794             {
1795                 if( !rbAlternativeMoveDirection )
1796                 {
1797                     rbAlternativeMoveDirection = true;
1798                     resetLabelPositionsToPreviousState();
1799                     return false;
1800                 }
1801             }
1802         }
1803     }
1804     return true;
1805 }
1806 
rearrangeLabelToAvoidOverlapIfRequested(const awt::Size & rPageSize)1807 void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSize )
1808 {
1809     ///this method is invoked by `ChartView::impl_createDiagramAndContent` for
1810     ///pie and donut charts after text label creation;
1811     ///it tries to rearrange labels only when the label placement type is
1812     ///`AVOID_OVERLAP`.
1813     // no need to do anything when we only have one label
1814     if (m_aLabelInfoList.size() < 2)
1815         return;
1816 
1817     ///check whether there are any labels that should be moved
1818     bool bMoveableFound = false;
1819     for (auto const& labelInfo : m_aLabelInfoList)
1820     {
1821         if(labelInfo.bMovementAllowed)
1822         {
1823             bMoveableFound = true;
1824             break;
1825         }
1826     }
1827     if(!bMoveableFound)
1828         return;
1829 
1830     double fPageDiagonaleLength = std::hypot(rPageSize.Width, rPageSize.Height);
1831     if( fPageDiagonaleLength == 0.0 )
1832         return;
1833 
1834     ///initialize next and previous member of `PieLabelInfo` objects
1835     auto aIt1 = m_aLabelInfoList.begin();
1836     auto aEnd = m_aLabelInfoList.end();
1837     std::vector< PieLabelInfo >::iterator aIt2 = aIt1;
1838     aIt1->pPrevious = &(*(m_aLabelInfoList.rbegin()));
1839     ++aIt2;
1840     for( ;aIt2!=aEnd; ++aIt1, ++aIt2 )
1841     {
1842         PieLabelInfo& rInfo1( *aIt1 );
1843         PieLabelInfo& rInfo2( *aIt2 );
1844         rInfo1.pNext = &rInfo2;
1845         rInfo2.pPrevious = &rInfo1;
1846     }
1847     aIt1->pNext = &(*(m_aLabelInfoList.begin()));
1848 
1849     ///detect overlaps and move
1850     sal_Int32 nMaxIterations = 50;
1851     while( detectLabelOverlapsAndMove( rPageSize ) && nMaxIterations > 0 )
1852         nMaxIterations--;
1853 
1854     ///create connection lines for the moved labels
1855     VLineProperties aVLineProperties;
1856     for (auto const& labelInfo : m_aLabelInfoList)
1857     {
1858         if( labelInfo.bMoved && labelInfo.bShowLeaderLine )
1859         {
1860             const basegfx::B2IRectangle aRect(lcl_getRect(labelInfo.xLabelGroupShape));
1861             sal_Int32 nX1 = labelInfo.aOuterPosition.getX();
1862             sal_Int32 nY1 = labelInfo.aOuterPosition.getY();
1863             const sal_Int32 nX2 = std::clamp(nX1, aRect.getMinX(), aRect.getMaxX());
1864             const sal_Int32 nY2 = std::clamp(nY1, aRect.getMinY(), aRect.getMaxY());
1865 
1866             //when the line is very short compared to the page size don't create one
1867             ::basegfx::B2DVector aLength(nX1-nX2, nY1-nY2);
1868             if( (aLength.getLength()/fPageDiagonaleLength) < 0.01 )
1869                 continue;
1870 
1871             drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } };
1872 
1873             if( labelInfo.xTextShape.is() )
1874             {
1875                 sal_Int32 nColor = 0;
1876                 labelInfo.xTextShape->SvxShape::getPropertyValue(u"CharColor"_ustr) >>= nColor;
1877                 if( nColor != -1 )//automatic font color does not work for lines -> fallback to black
1878                     aVLineProperties.Color <<= nColor;
1879             }
1880             ShapeFactory::createLine2D( labelInfo.xTextTarget, aPoints, &aVLineProperties );
1881         }
1882     }
1883 }
1884 
1885 
1886 /** Handle the placement of the label in the best fit case:
1887  *  the routine try to place the label inside the related pie slice,
1888  *  in case of success it returns true else returns false.
1889  *
1890  *  Notation:
1891  *  C: the pie center
1892  *  s: the bisector ray of the current pie slice
1893  *  alpha: the angle between the horizontal axis and the bisector ray s
1894  *  N: the vertex of the label b.b. which is nearest to C
1895  *  F: the vertex of the label b.b. not adjacent to N; F lies on the pie border
1896  *  P, Q: the intersection points between the label b.b. and the bisector ray s;
1897  *        P is the one at minimum distance respect with C
1898  *  e: the edge of the label b.b. where P lies (the nearest edge to C)
1899  *  M: the vertex of e that is not N
1900  *  G: the vertex of the label b.b. which is adjacent to N and that is not M
1901  *  beta: the angle MPF
1902  *  theta: the angle CPF
1903  *
1904  *
1905  *     |
1906  *     |                                /s
1907  *     |                               /
1908  *     |                              /
1909  *     |  G _________________________/____________________________ F
1910  *     |   |                        /Q                          ..|
1911  *     |   |                       /                         . .  |
1912  *     |   |                      /                       .  .    |
1913  *     |   |                     /                     .   .      |
1914  *     |   |                    /                   .    .        |
1915  *     |   |                   /                 .     .          |
1916  *     |   |                  /              d.      .            |
1917  *     |   |                 /             .       .              |
1918  *     |   |                /           .        .                |
1919  *     |   |               /         .         .                  |
1920  *     |   |              /       .          .                    |
1921  *     |   |             /     .           .                      |
1922  *     |   |            /   .            .                        |
1923  *     |   |           / .  \ beta     .                          |
1924  *     |   |__________/._\___|_______.____________________________|
1925  *     |  N          /P  /         .                               M
1926  *     |            /___/theta   .
1927  *     |           /           .
1928  *     |          /          . r
1929  *     |         /         .
1930  *     |        /        .
1931  *     |       /       .
1932  *     |      /      .
1933  *     |     /     .
1934  *     |    /    .
1935  *     |   /   .
1936  *     |  /  .
1937  *     | /\. alpha
1938  *   __|/__|_____________________________________________________________
1939  *     |C
1940  *     |
1941  *
1942  *
1943  *  When alpha = 45k (k integer) s crosses the label b.b. at N exactly.
1944  *  In such a case the nearest edge e is defined as the edge having N as the
1945  *  start vertex and that is covered in the counterclockwise direction when
1946  *  we move from N to the adjacent vertex.
1947  *
1948  *  The nearest vertex N is:
1949  *   1. the bottom left vertex when 0 < alpha < 90
1950  *   2. the bottom right vertex when 90 < alpha < 180
1951  *   3. the top right vertex when 180 < alpha < 270
1952  *   4. the top left vertex when 270 < alpha < 360.
1953  *
1954  *  The nearest edge e is:
1955  *   1. the left edge when −45 < alpha < 45
1956  *   2. the bottom edge when 45 < alpha <135
1957  *   3. the right edge when 135 < alpha < 225
1958  *   4. the top edge when 225 < alpha < 315.
1959  *
1960  **/
performLabelBestFitInnerPlacement(ShapeParam & rShapeParam,PieLabelInfo const & rPieLabelInfo,double fRadiusScale,const::basegfx::B3DVector & aShift)1961 bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam,
1962         PieLabelInfo const & rPieLabelInfo, double fRadiusScale,
1963         const ::basegfx::B3DVector& aShift)
1964 {
1965     SAL_INFO( "chart2.pie.label.bestfit.inside",
1966               "** PieChart::performLabelBestFitInnerPlacement invoked **" );
1967 
1968     // get pie slice properties
1969     double fStartAngleDeg = NormAngle360(rShapeParam.mfUnitCircleStartAngleDegree);
1970     double fWidthAngleDeg = rShapeParam.mfUnitCircleWidthAngleDegree;
1971     double fHalfWidthAngleDeg = fWidthAngleDeg / 2.0;
1972     double fBisectingRayAngleDeg = NormAngle360(fStartAngleDeg + fHalfWidthAngleDeg);
1973 
1974     // get the middle point of the arc representing the pie slice border
1975     double fLogicZ = rShapeParam.mfLogicZ + 1.0;
1976     drawing::Position3D aUnitCirclePt = m_aPosHelper.transformUnitCircleToScene(
1977                     fBisectingRayAngleDeg,
1978                     rShapeParam.mfUnitCircleOuterRadius * fRadiusScale,
1979                     fLogicZ,
1980                     aShift);
1981     awt::Point aMiddleArcPoint = PlottingPositionHelper::transformSceneToScreenPosition(
1982             aUnitCirclePt, m_xLogicTarget, m_nDimension );
1983 
1984     // compute the pie radius
1985     basegfx::B2IVector aPieCenter = rPieLabelInfo.aOrigin;
1986     basegfx::B2IVector aRadiusVector(
1987             aMiddleArcPoint.X - aPieCenter.getX(),
1988             aMiddleArcPoint.Y - aPieCenter.getY() );
1989     double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector);
1990     double fPieRadius = sqrt( fSquaredPieRadius );
1991 
1992     // the bb is moved as much as possible near to the border of the pie,
1993     // anyway a small offset from the border is present (0.025 * pie radius)
1994     const double fPieBorderOffset = 0.025;
1995     fPieRadius *= (1 - fPieBorderOffset);
1996 
1997     SAL_INFO( "chart2.pie.label.bestfit.inside",
1998               "    pie sector:" );
1999     SAL_INFO( "chart2.pie.label.bestfit.inside",
2000               "      start angle = " << fStartAngleDeg );
2001     SAL_INFO( "chart2.pie.label.bestfit.inside",
2002               "      angle width = " << fWidthAngleDeg );
2003     SAL_INFO( "chart2.pie.label.bestfit.inside",
2004               "      bisecting ray angle = " << fBisectingRayAngleDeg );
2005     SAL_INFO( "chart2.pie.label.bestfit.inside",
2006               "      pie radius = " << fPieRadius );
2007     SAL_INFO( "chart2.pie.label.bestfit.inside",
2008               "      pie center = " << rPieLabelInfo.aOrigin );
2009     SAL_INFO( "chart2.pie.label.bestfit.inside",
2010               "      middle arc point = (" << aMiddleArcPoint.X << ","
2011                                            << aMiddleArcPoint.Y << ")" );
2012     SAL_INFO( "chart2.pie.label.bestfit.inside",
2013               "    label bounding box:" );
2014     SAL_INFO( "chart2.pie.label.bestfit.inside",
2015               "      old anchor point = " << rPieLabelInfo.aFirstPosition );
2016 
2017 
2018     if( fPieRadius == 0.0 )
2019         return false;
2020 
2021     // get label b.b. width and height
2022     ::basegfx::B2IRectangle aBb( lcl_getRect( rPieLabelInfo.xLabelGroupShape ) );
2023     double fLabelWidth = aBb.getWidth();
2024     double fLabelHeight = aBb.getHeight();
2025 
2026     // -45 <= fAlphaDeg < 315
2027     double fAlphaDeg = NormAngle360(fBisectingRayAngleDeg + 45) - 45;
2028     double fAlphaRad = basegfx::deg2rad(fAlphaDeg);
2029 
2030     // compute nearest edge index
2031     // 0 left
2032     // 1 bottom
2033     // 2 right
2034     // 3 top
2035     int nSectorIndex = floor( (fAlphaDeg + 45) / 45.0 );
2036     int nNearestEdgeIndex = nSectorIndex / 2;
2037 
2038     // compute lengths of the nearest edge and of the orthogonal edges
2039     double fNearestEdgeLength = fLabelWidth;
2040     double fOrthogonalEdgeLength = fLabelHeight;
2041     basegfx::Axis2D eAxis = basegfx::Axis2D::X;
2042     basegfx::Axis2D eOrthogonalAxis = basegfx::Axis2D::Y;
2043     if( nNearestEdgeIndex % 2 == 0 ) // nearest edge is vertical
2044     {
2045         fNearestEdgeLength = fLabelHeight;
2046         fOrthogonalEdgeLength = fLabelWidth;
2047         eAxis = basegfx::Axis2D::Y;
2048         eOrthogonalAxis = basegfx::Axis2D::X;
2049     }
2050 
2051     // compute the distance between N and P
2052     // such a distance is piece wise linear respect with alpha:
2053     // given 45k <= alpha < 45(k+1) we have
2054     // when k is even: d(N,P) = (length(e) / 2) * (1 - (alpha - 45k)/45)
2055     // when k is odd: d(N,P) = (length(e) / 2) * (1 - (45(k+1) - alpha)/45)
2056     int nIndex = nSectorIndex -1;  // nIndex = -1...6
2057     double fIndexMod2 = (nIndex + 8) % 2; // fIndexMod2 must be non negative
2058     double fSgn = 2.0 * (fIndexMod2 - 0.5); // 0 -> -1, 1 -> 1
2059     double fDistanceNP = (fNearestEdgeLength / 2.0) * (1 + fSgn * ((fAlphaDeg - 45 * (nIndex + fIndexMod2)) / 45.0));
2060     double fDistancePM = fNearestEdgeLength - fDistanceNP;
2061 
2062     // compute the length of the diagonal vector d,
2063     // that is the distance between P and F
2064     double fDistancePF = std::hypot(fDistancePM, fOrthogonalEdgeLength);
2065 
2066     SAL_INFO( "chart2.pie.label.bestfit.inside",
2067               "      width = " << fLabelWidth );
2068     SAL_INFO( "chart2.pie.label.bestfit.inside",
2069               "      height = " <<  fLabelHeight );
2070     SAL_INFO( "chart2.pie.label.bestfit.inside",
2071               "      nearest edge index = " << nNearestEdgeIndex );
2072     SAL_INFO( "chart2.pie.label.bestfit.inside",
2073               "      alpha = " << fAlphaDeg );
2074     SAL_INFO( "chart2.pie.label.bestfit.inside",
2075               "      distance(N,P) = " << fDistanceNP );
2076     SAL_INFO( "chart2.pie.label.bestfit.inside",
2077               "        nIndex = " << nIndex );
2078     SAL_INFO( "chart2.pie.label.bestfit.inside",
2079               "        fIndexMod2 = " << fIndexMod2 );
2080     SAL_INFO( "chart2.pie.label.bestfit.inside",
2081               "        fSgn = " << fSgn );
2082     SAL_INFO( "chart2.pie.label.bestfit.inside",
2083               "      distance(P,F) = " << fDistancePF );
2084 
2085 
2086     // we check that the condition length(d) <= pie radius holds
2087     if (fDistancePF > fPieRadius)
2088     {
2089         return false;
2090     }
2091 
2092     // compute beta: the angle of the diagonal vector d,
2093     // that is, the angle in P respect with the triangle PMF;
2094     // since both arguments are non negative the returned value is in [0, PI/2]
2095     double fBetaRad = atan2( fOrthogonalEdgeLength, fDistancePM );
2096 
2097     // compute the theta angle, that is the angle in P
2098     // respect with the triangle CFP;
2099     // when the second intersection edge is opposite to the nearest edge,
2100     // theta depends on alpha and beta according to the following relation:
2101     // theta = f(alpha, beta) = s * alpha + 90 * (1 - s * i) + beta
2102     // where i is the nearest edge index and s is the sign of (alpha' - 45),
2103     // with alpha' = (alpha + 45) mod 90;
2104     // when the second intersection edge is adjacent to the nearest edge,
2105     // we have theta = 360 - f(alpha, beta);
2106     // note that in the former case 0 <= f(alpha, beta) <= 180,
2107     // whilst in the latter case 180 <= f(alpha, beta) <= 360;
2108     double fAlphaMod90 = fmod( fAlphaDeg + 45, 90.0 ) - 45;
2109     double fSign = fAlphaMod90 == 0.0
2110                        ? 0.0
2111                        : ( fAlphaMod90 < 0 ) ? -1.0 : 1.0;
2112     double fThetaRad = fSign * fAlphaRad + M_PI_2 * (1 - fSign * nNearestEdgeIndex) + fBetaRad;
2113     if( fThetaRad > M_PI )
2114     {
2115         fThetaRad = 2 * M_PI - fThetaRad;
2116     }
2117 
2118     // compute the length of the positional vector,
2119     // that is the distance between C and P
2120     double fDistanceCP;
2121     // when the bisector ray intersects the b.b. in F we have theta mod 180 == 0
2122     if( fmod(fThetaRad, M_PI) == 0.0 )
2123     {
2124         fDistanceCP = fPieRadius - fDistancePF;
2125     }
2126     else // general case
2127     {
2128         // we can compute d(C,P) by applying some trigonometric formula to
2129         // the triangle CFP : we know length(d) and length(r) = r and we have
2130         // computed the angle in P (theta); so named delta the angle in C and
2131         // gamma the angle in F, by the relation:
2132         //
2133         //                r         d(P,F)     d(C,P)
2134         //            --------- = --------- = ---------
2135         //            sin theta   sin delta   sin gamma
2136         //
2137         // we get the wanted distance
2138         double fSinTheta = sin( fThetaRad );
2139         double fSinDelta = fDistancePF * fSinTheta / fPieRadius;
2140         double fDeltaRad = asin( fSinDelta );
2141         double fGammaRad = M_PI - (fThetaRad + fDeltaRad);
2142         double fSinGamma = sin( fGammaRad );
2143         fDistanceCP = fPieRadius * fSinGamma / fSinTheta;
2144     }
2145 
2146     // define the positional vector
2147     basegfx::B2DVector aPositionalVector( cos(fAlphaRad), sin(fAlphaRad) );
2148     aPositionalVector.setLength(fDistanceCP);
2149 
2150     // we define a direction vector in order to know
2151     // in which quadrant we are working
2152     basegfx::B2DVector aDirection(1.0, 1.0);
2153     if( 90 <= fBisectingRayAngleDeg && fBisectingRayAngleDeg < 270 )
2154     {
2155         aDirection.setX(-1.0);
2156     }
2157     if( fBisectingRayAngleDeg >= 180 )
2158     {
2159         aDirection.setY(-1.0);
2160     }
2161 
2162     // compute vertices N, M and G respect with pie center C
2163     basegfx::B2DVector aNearestVertex(aPositionalVector);
2164     aNearestVertex.set(eAxis, aNearestVertex.get(eAxis) - aDirection.get(eAxis) * fDistanceNP);
2165     basegfx::B2DVector aVertexM(aNearestVertex);
2166     aVertexM.set(eAxis, aVertexM.get(eAxis) + aDirection.get(eAxis) * fNearestEdgeLength);
2167     basegfx::B2DVector aVertexG(aNearestVertex);
2168     aVertexG.set(eOrthogonalAxis, aVertexG.get(eOrthogonalAxis) + aDirection.get(eOrthogonalAxis) * fOrthogonalEdgeLength);
2169 
2170     SAL_INFO( "chart2.pie.label.bestfit.inside",
2171               "      beta = " << basegfx::rad2deg(fBetaRad) );
2172     SAL_INFO( "chart2.pie.label.bestfit.inside",
2173               "      theta = " << basegfx::rad2deg(fThetaRad) );
2174     SAL_INFO( "chart2.pie.label.bestfit.inside",
2175               "        fAlphaMod90 = " << fAlphaMod90 );
2176     SAL_INFO( "chart2.pie.label.bestfit.inside",
2177               "        fSign = " << fSign );
2178     SAL_INFO( "chart2.pie.label.bestfit.inside",
2179               "      distance(C,P) = " << fDistanceCP );
2180     SAL_INFO( "chart2.pie.label.bestfit.inside",
2181               "      direction vector = " << aDirection );
2182     SAL_INFO( "chart2.pie.label.bestfit.inside",
2183               "      N = " << aNearestVertex );
2184     SAL_INFO( "chart2.pie.label.bestfit.inside",
2185               "      M = " << aVertexM );
2186     SAL_INFO( "chart2.pie.label.bestfit.inside",
2187               "      G = " << aVertexG );
2188 
2189     // in order to be able to place the label inside the pie slice we need
2190     // to check that each angle between s and the ray starting from C and
2191     // passing through a b.b. vertex is less than half width of the pie slice;
2192     // when the nearest edge e crosses a Cartesian axis it is sufficient
2193     // to test only the vertices belonging to e, else we need to test
2194     // the 2 vertices that aren't either N or F. Note that if a b.b. edge
2195     // crosses a Cartesian axis then it is the nearest edge to C
2196 
2197     // check the angle between CP and CM
2198     double fAngleRad = aPositionalVector.angle(aVertexM);
2199     double fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad));
2200     if( fAngleDeg > 180 )  // in case the wrong angle has been computed
2201         fAngleDeg = 360 - fAngleDeg;
2202     SAL_INFO( "chart2.pie.label.bestfit.inside",
2203               "      angle between CP and CM: " << fAngleDeg );
2204     if( fAngleDeg > fHalfWidthAngleDeg )
2205     {
2206         return false;
2207     }
2208 
2209     if( ( aNearestVertex.get(eAxis) >= 0 && aVertexM.get(eAxis) <= 0 )
2210             || ( aNearestVertex.get(eAxis) <= 0 && aVertexM.get(eAxis) >= 0 ) )
2211     {
2212         // check the angle between CP and CN
2213         fAngleRad = aPositionalVector.angle(aNearestVertex);
2214         fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad));
2215         if( fAngleDeg > 180 )  // in case the wrong angle has been computed
2216             fAngleDeg = 360 - fAngleDeg;
2217         SAL_INFO( "chart2.pie.label.bestfit.inside",
2218                   "      angle between CP and CN: " << fAngleDeg );
2219         if( fAngleDeg > fHalfWidthAngleDeg )
2220         {
2221             return false;
2222         }
2223     }
2224     else
2225     {
2226         // check the angle between CP and CG
2227         fAngleRad = aPositionalVector.angle(aVertexG);
2228         fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad));
2229         if( fAngleDeg > 180 )  // in case the wrong angle has been computed
2230             fAngleDeg = 360 - fAngleDeg;
2231         SAL_INFO( "chart2.pie.label.bestfit.inside",
2232                   "      angle between CP and CG: " << fAngleDeg );
2233         if( fAngleDeg > fHalfWidthAngleDeg )
2234         {
2235             return false;
2236         }
2237     }
2238 
2239     // compute the b.b. center respect with the pie center
2240     basegfx::B2DVector aBBCenter(aNearestVertex);
2241     aBBCenter.set(eAxis, aBBCenter.get(eAxis) + aDirection.get(eAxis) * fNearestEdgeLength / 2);
2242     aBBCenter.set(eOrthogonalAxis, aBBCenter.get(eOrthogonalAxis) + aDirection.get(eOrthogonalAxis) * fOrthogonalEdgeLength / 2);
2243 
2244     // compute the b.b. anchor point
2245     basegfx::B2IVector aNewAnchorPoint = aPieCenter;
2246     aNewAnchorPoint.setX(aNewAnchorPoint.getX() + floor(aBBCenter.getX()));
2247     aNewAnchorPoint.setY(aNewAnchorPoint.getY() - floor(aBBCenter.getY())); // the Y axis on the screen points downward
2248 
2249     // compute the translation vector for moving the label from the current
2250     // screen position to the new one
2251     basegfx::B2IVector aTranslationVector = aNewAnchorPoint - rPieLabelInfo.aFirstPosition;
2252 
2253     // compute the new screen position and move the label
2254     // XShape::getPosition returns the top left vertex of the b.b. of the shape
2255     awt::Point aOldPos( rPieLabelInfo.xLabelGroupShape->getPosition() );
2256     awt::Point aNewPos( aOldPos.X + aTranslationVector.getX(),
2257                         aOldPos.Y + aTranslationVector.getY() );
2258     rPieLabelInfo.xLabelGroupShape->setPosition(aNewPos);
2259 
2260     SAL_INFO( "chart2.pie.label.bestfit.inside",
2261               "      center = " <<  aBBCenter );
2262     SAL_INFO( "chart2.pie.label.bestfit.inside",
2263               "      new anchor point = " << aNewAnchorPoint );
2264     SAL_INFO( "chart2.pie.label.bestfit.inside",
2265               "      translation vector = " <<  aTranslationVector );
2266     SAL_INFO( "chart2.pie.label.bestfit.inside",
2267               "      old position = (" << aOldPos.X << "," << aOldPos.Y << ")" );
2268     SAL_INFO( "chart2.pie.label.bestfit.inside",
2269               "      new position = (" << aNewPos.X << "," << aNewPos.Y << ")" );
2270 
2271     return true;
2272 }
2273 
2274 //=======================
2275 // class PieDataSrc
2276 //=======================
getData(const VDataSeries * pSeries,sal_Int32 nPtIdx,enum SubPieType eType) const2277 double PieDataSrc::getData(const VDataSeries* pSeries, sal_Int32 nPtIdx,
2278        [[maybe_unused]] enum SubPieType eType) const
2279 {
2280     return fabs(pSeries->getYValue( nPtIdx ));
2281 }
2282 
getNPoints(const VDataSeries * pSeries,enum SubPieType eType) const2283 sal_Int32 PieDataSrc::getNPoints(const VDataSeries* pSeries,
2284             [[maybe_unused]] enum SubPieType eType) const
2285 {
2286     assert(eType == SubPieType::NONE);
2287     return pSeries->getTotalPointCount();
2288 }
2289 
getProps(const VDataSeries * pSeries,sal_Int32 nPtIdx,enum SubPieType eType) const2290 uno::Reference< beans::XPropertySet > PieDataSrc::getProps(
2291             const VDataSeries* pSeries, sal_Int32 nPtIdx,
2292             [[maybe_unused]] enum SubPieType eType) const
2293 {
2294     assert(eType == SubPieType::NONE);
2295     return pSeries->getPropertiesOfPoint(nPtIdx);
2296 }
2297 
2298 
2299 //=======================
2300 // class OfPieDataSrc
2301 //=======================
2302 
2303 // Support data splits only of the type "last n entries go in right subchart",
2304 // for now.
2305 // TODO
2306 
getNPoints(const VDataSeries * pSeries,enum SubPieType eType) const2307 sal_Int32 OfPieDataSrc::getNPoints(const VDataSeries* pSeries,
2308             enum SubPieType eType) const
2309 {
2310     if (eType == SubPieType::LEFT) {
2311         return pSeries->getTotalPointCount() - m_nSplitPos + 1;
2312     } else {
2313         assert(eType == SubPieType::RIGHT);
2314         return m_nSplitPos;
2315     }
2316 }
2317 
getData(const VDataSeries * pSeries,sal_Int32 nPtIdx,enum SubPieType eType) const2318 double OfPieDataSrc::getData(const VDataSeries* pSeries, sal_Int32 nPtIdx,
2319             enum SubPieType eType) const
2320 {
2321     const sal_Int32 n = pSeries->getTotalPointCount() - m_nSplitPos;
2322     if (eType == SubPieType::LEFT) {
2323         // nPtIdx should be in [0, n]
2324         if (nPtIdx < n) {
2325             return fabs(pSeries->getYValue( nPtIdx ));
2326         } else {
2327             // composite wedge
2328             assert(nPtIdx == n);
2329             double total = 0;
2330             for (sal_Int32 i = n; i < n + m_nSplitPos; ++i) {
2331                 total += pSeries->getYValue(i);
2332             }
2333             return total;
2334         }
2335     } else {
2336         assert(eType == SubPieType::RIGHT);
2337         return fabs(pSeries->getYValue(nPtIdx + n));
2338     }
2339 }
2340 
getProps(const VDataSeries * pSeries,sal_Int32 nPtIdx,enum SubPieType eType) const2341 uno::Reference< beans::XPropertySet > OfPieDataSrc::getProps(
2342             const VDataSeries* pSeries, sal_Int32 nPtIdx,
2343             enum SubPieType eType) const
2344 {
2345     const sal_Int32 nPts = pSeries->getTotalPointCount();
2346     const sal_Int32 n = nPts - m_nSplitPos;
2347     if (eType == SubPieType::LEFT) {
2348         // nPtIdx should be in [0, n]
2349         if (nPtIdx < n) {
2350             return pSeries->getPropertiesOfPoint( nPtIdx );
2351         } else {
2352             // The aggregated wedge
2353             assert(nPtIdx == n);
2354             return pSeries->getPropertiesOfPoint(nPts);
2355         }
2356     } else {
2357         assert(eType == SubPieType::RIGHT);
2358         return pSeries->getPropertiesOfPoint(nPtIdx + n);
2359     }
2360 }
2361 
2362 } //namespace chart
2363 
2364 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
2365