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