diff options
Diffstat (limited to 'chart2/source/view/charttypes/PieChart.cxx')
-rw-r--r-- | chart2/source/view/charttypes/PieChart.cxx | 1719 |
1 files changed, 1719 insertions, 0 deletions
diff --git a/chart2/source/view/charttypes/PieChart.cxx b/chart2/source/view/charttypes/PieChart.cxx new file mode 100644 index 000000000..a81428c24 --- /dev/null +++ b/chart2/source/view/charttypes/PieChart.cxx @@ -0,0 +1,1719 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <BaseGFXHelper.hxx> +#include <VLineProperties.hxx> +#include "PieChart.hxx" +#include <PlottingPositionHelper.hxx> +#include <ShapeFactory.hxx> +#include <PolarLabelPositionHelper.hxx> +#include <CommonConverters.hxx> +#include <ObjectIdentifier.hxx> +#include <ChartType.hxx> + +#include <com/sun/star/chart/DataLabelPlacement.hpp> +#include <com/sun/star/chart2/XColorScheme.hpp> + +#include <com/sun/star/drawing/XShapes.hpp> +#include <sal/log.hxx> +#include <osl/diagnose.h> +#include <tools/diagnose_ex.h> +#include <tools/helpers.hxx> + +#include <limits> +#include <memory> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +namespace chart { + +struct PieChart::ShapeParam +{ + /** the start angle of the slice + */ + double mfUnitCircleStartAngleDegree; + + /** the angle width of the slice + */ + double mfUnitCircleWidthAngleDegree; + + /** the normalized outer radius of the ring the slice belongs to. + */ + double mfUnitCircleOuterRadius; + + /** the normalized inner radius of the ring the slice belongs to + */ + double mfUnitCircleInnerRadius; + + /** relative distance offset of a slice from the pie center; + * this parameter is used for instance when the user performs manual + * dragging of a slice (the drag operation is possible only for slices that + * belong to the outer ring and only along the ray bisecting the slice); + * the value for the given entry in the data series is obtained by the + * `Offset` property attached to each entry; note that the value + * provided by the `Offset` property is used both as a logical value in + * `PiePositionHelper::getInnerAndOuterRadius` and as a percentage value in + * the `PieChart::createDataPoint` and `PieChart::createTextLabelShape` + * methods; since the logical height of a ring is always 1, this duality + * does not cause any incorrect behavior; + */ + double mfExplodePercentage; + + /** sum of all Y values in a single series + */ + double mfLogicYSum; + + /** for 3D pie chart: label z coordinate + */ + double mfLogicZ; + + /** for 3D pie chart: height + */ + double mfDepth; + + ShapeParam() : + mfUnitCircleStartAngleDegree(0.0), + mfUnitCircleWidthAngleDegree(0.0), + mfUnitCircleOuterRadius(0.0), + mfUnitCircleInnerRadius(0.0), + mfExplodePercentage(0.0), + mfLogicYSum(0.0), + mfLogicZ(0.0), + mfDepth(0.0) {} +}; + +namespace +{ +::basegfx::B2IRectangle lcl_getRect(const rtl::Reference<SvxShape>& xShape) +{ + ::basegfx::B2IRectangle aRect; + if (xShape.is()) + aRect = BaseGFXHelper::makeRectangle(xShape->getPosition(), xShape->getSize()); + return aRect; +} + +bool lcl_isInsidePage(const awt::Point& rPos, const awt::Size& rSize, const awt::Size& rPageSize) +{ + if (rPos.X < 0 || rPos.Y < 0) + return false; + if ((rPos.X + rSize.Width) > rPageSize.Width) + return false; + if ((rPos.Y + rSize.Height) > rPageSize.Height) + return false; + return true; +} + +} //end anonymous namespace + +class PiePositionHelper : public PolarPlottingPositionHelper +{ +public: + PiePositionHelper( double fAngleDegreeOffset ); + + bool getInnerAndOuterRadius( double fCategoryX, double& fLogicInnerRadius, double& fLogicOuterRadius, bool bUseRings, double fMaxOffset ) const; + +public: + //Distance between different category rings, seen relative to width of a ring: + double m_fRingDistance; //>=0 m_fRingDistance=1 --> distance == width +}; + +PiePositionHelper::PiePositionHelper( double fAngleDegreeOffset ) + : m_fRingDistance(0.0) +{ + m_fRadiusOffset = 0.0; + m_fAngleDegreeOffset = fAngleDegreeOffset; +} + +/** Compute the outer and the inner radius for the current ring (not for the + * whole donut!), in general it is: + * inner_radius = (ring_index + 1) - 0.5 + max_offset, + * outer_radius = (ring_index + 1) + 0.5 + max_offset. + * When orientation for the radius axis is reversed these values are swapped. + * (Indeed the orientation for the radius axis is always reversed! + * See `PieChartTypeTemplate::adaptScales`.) + * The maximum relative offset (see notes for `PieChart::getMaxOffset`) is + * added to both the inner and the outer radius. + * It returns true if the ring is visible (that is not out of the radius + * axis scale range). + */ +bool PiePositionHelper::getInnerAndOuterRadius( double fCategoryX + , double& fLogicInnerRadius, double& fLogicOuterRadius + , bool bUseRings, double fMaxOffset ) const +{ + if( !bUseRings ) + fCategoryX = 1.0; + + double fLogicInner = fCategoryX -0.5+m_fRingDistance/2.0; + double fLogicOuter = fCategoryX +0.5-m_fRingDistance/2.0; + + if( !isMathematicalOrientationRadius() ) + { + //in this case the given getMaximumX() was not correct instead the minimum should have been smaller by fMaxOffset + //but during getMaximumX and getMimumX we do not know the axis orientation + fLogicInner += fMaxOffset; + fLogicOuter += fMaxOffset; + } + + if( fLogicInner >= getLogicMaxX() ) + return false; + if( fLogicOuter <= getLogicMinX() ) + return false; + + if( fLogicInner < getLogicMinX() ) + fLogicInner = getLogicMinX(); + if( fLogicOuter > getLogicMaxX() ) + fLogicOuter = getLogicMaxX(); + + fLogicInnerRadius = fLogicInner; + fLogicOuterRadius = fLogicOuter; + if( !isMathematicalOrientationRadius() ) + std::swap(fLogicInnerRadius,fLogicOuterRadius); + return true; +} + +PieChart::PieChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bExcludingPositioning ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount ) + , m_pPosHelper( new PiePositionHelper( (m_nDimension==3) ? 0.0 : 90.0 ) ) + , m_bUseRings(false) + , m_bSizeExcludesLabelsAndExplodedSegments(bExcludingPositioning) + , m_fMaxOffset(std::numeric_limits<double>::quiet_NaN()) +{ + PlotterBase::m_pPosHelper = m_pPosHelper.get(); + VSeriesPlotter::m_pMainPosHelper = m_pPosHelper.get(); + m_pPosHelper->m_fRadiusOffset = 0.0; + m_pPosHelper->m_fRingDistance = 0.0; + + if( !xChartTypeModel.is() ) + return; + + try + { + xChartTypeModel->getPropertyValue( "UseRings") >>= m_bUseRings; + if( m_bUseRings ) + { + m_pPosHelper->m_fRadiusOffset = 1.0; + if( nDimensionCount==3 ) + m_pPosHelper->m_fRingDistance = 0.1; + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } +} + +PieChart::~PieChart() +{ +} + +void PieChart::setScales( std::vector< ExplicitScaleData >&& rScales, bool /* bSwapXAndYAxis */ ) +{ + OSL_ENSURE(m_nDimension<=static_cast<sal_Int32>(rScales.size()),"Dimension of Plotter does not fit two dimension of given scale sequence"); + m_pPosHelper->setScales( std::move(rScales), true ); +} + +drawing::Direction3D PieChart::getPreferredDiagramAspectRatio() const +{ + if( m_nDimension == 3 ) + return drawing::Direction3D(1,1,0.10); + return drawing::Direction3D(1,1,1); +} + +bool PieChart::shouldSnapRectToUsedArea() +{ + return true; +} + +rtl::Reference<SvxShape> PieChart::createDataPoint( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget, + const uno::Reference<beans::XPropertySet>& xObjectProperties, + const ShapeParam& rParam, + const sal_Int32 nPointCount, + const bool bConcentricExplosion) +{ + //transform position: + drawing::Direction3D aOffset; + double fExplodedInnerRadius = rParam.mfUnitCircleInnerRadius; + double fExplodedOuterRadius = rParam.mfUnitCircleOuterRadius; + double fStartAngle = rParam.mfUnitCircleStartAngleDegree; + double fWidthAngle = rParam.mfUnitCircleWidthAngleDegree; + + if (rParam.mfExplodePercentage != 0.0) { + double fRadius = (fExplodedOuterRadius-fExplodedInnerRadius)*rParam.mfExplodePercentage; + + if (bConcentricExplosion) { + + // For concentric explosion, increase the radius but retain the original + // arc length of all ring segments together. This results in a gap + // that's evenly divided among all segments, assuming they all have + // the same explosion percentage + assert(fExplodedInnerRadius >= 0 && fExplodedOuterRadius > 0); + double fAngleRatio = (fExplodedInnerRadius + fExplodedOuterRadius) / + (fExplodedInnerRadius + fExplodedOuterRadius + 2 * fRadius); + + assert(nPointCount > 0); + double fAngleGap = 360 * (1.0 - fAngleRatio) / nPointCount; + fStartAngle += fAngleGap / 2; + fWidthAngle -= fAngleGap; + + fExplodedInnerRadius += fRadius; + fExplodedOuterRadius += fRadius; + + } else { + // For the non-concentric explosion case, keep the original radius + // but shift the circle origin + double fAngle = fStartAngle + fWidthAngle/2.0; + + drawing::Position3D aOrigin = m_pPosHelper->transformUnitCircleToScene(0, 0, rParam.mfLogicZ); + drawing::Position3D aNewOrigin = m_pPosHelper->transformUnitCircleToScene(fAngle, fRadius, rParam.mfLogicZ); + aOffset = aNewOrigin - aOrigin; + } + } + + //create point + rtl::Reference<SvxShape> xShape; + if(m_nDimension==3) + { + xShape = ShapeFactory::createPieSegment( xTarget + , fStartAngle, fWidthAngle + , fExplodedInnerRadius, fExplodedOuterRadius + , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() ) + , rParam.mfDepth ); + } + else + { + xShape = ShapeFactory::createPieSegment2D( xTarget + , fStartAngle, fWidthAngle + , fExplodedInnerRadius, fExplodedOuterRadius + , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() ) ); + } + PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + return xShape; +} + +void PieChart::createTextLabelShape( + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + VDataSeries& rSeries, sal_Int32 nPointIndex, ShapeParam& rParam ) +{ + if (!rSeries.getDataPointLabelIfLabel(nPointIndex)) + // There is no text label for this data point. Nothing to do. + return; + + ///by using the `mfExplodePercentage` parameter a normalized offset is added + ///to both normalized radii. (See notes for + ///`PolarPlottingPositionHelper::transformToRadius`, especially example 3, + ///and related comments). + if (rParam.mfExplodePercentage != 0.0) + { + double fExplodeOffset = (rParam.mfUnitCircleOuterRadius-rParam.mfUnitCircleInnerRadius)*rParam.mfExplodePercentage; + rParam.mfUnitCircleInnerRadius += fExplodeOffset; + rParam.mfUnitCircleOuterRadius += fExplodeOffset; + } + + ///get the required label placement type. Available placements are + ///`AVOID_OVERLAP`, `CENTER`, `OUTSIDE` and `INSIDE`; + sal_Int32 nLabelPlacement = rSeries.getLabelPlacement( + nPointIndex, m_xChartTypeModel, m_pPosHelper->isSwapXAndY()); + + ///when the placement is of `AVOID_OVERLAP` type a later rearrangement of + ///the label position is allowed; the `createTextLabelShape` treats the + ///`AVOID_OVERLAP` as if it was of `CENTER` type; + + double nVal = rSeries.getYValue(nPointIndex); + //AVOID_OVERLAP is in fact "Best fit" in the UI. + bool bMovementAllowed = nLabelPlacement == css::chart::DataLabelPlacement::AVOID_OVERLAP + || nLabelPlacement == css::chart::DataLabelPlacement::CUSTOM; + if( bMovementAllowed ) + nLabelPlacement = css::chart::DataLabelPlacement::CENTER; + + ///for `OUTSIDE` (`INSIDE`) label placements an offset of 150 (-150), in the + ///radius direction, is added to the final screen position of the label + ///anchor point. This is required in order to ensure that the label is + ///completely outside (inside) the related slice. Indeed this value should + ///depend on the font height; + ///pay attention: 150 is not a big offset, in fact the screen position + ///coordinates for label anchor points are in the 10000-20000 range, hence + ///these are coordinates of a virtual screen and 150 is a small value; + LabelAlignment eAlignment(LABEL_ALIGN_CENTER); + sal_Int32 nScreenValueOffsetInRadiusDirection = 0 ; + if( nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE ) + nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? 150 : 0;//todo maybe calculate this font height dependent + else if( nLabelPlacement == css::chart::DataLabelPlacement::INSIDE ) + nScreenValueOffsetInRadiusDirection = (m_nDimension!=3) ? -150 : 0;//todo maybe calculate this font height dependent + + ///the scene position of the label anchor point is calculated (see notes for + ///`PolarLabelPositionHelper::getLabelScreenPositionAndAlignmentForUnitCircleValues`), + ///and immediately transformed into the screen position. + PolarLabelPositionHelper aPolarPosHelper(m_pPosHelper.get(),m_nDimension,m_xLogicTarget); + awt::Point aScreenPosition2D( + aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(eAlignment, nLabelPlacement + , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree + , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius, rParam.mfLogicZ+0.5, 0 )); + + ///the screen position of the pie/donut center is calculated. + PieLabelInfo aPieLabelInfo; + aPieLabelInfo.aFirstPosition = basegfx::B2IVector( aScreenPosition2D.X, aScreenPosition2D.Y ); + awt::Point aOrigin( aPolarPosHelper.transformSceneToScreenPosition( m_pPosHelper->transformUnitCircleToScene( 0.0, 0.0, rParam.mfLogicZ+1.0 ) ) ); + aPieLabelInfo.aOrigin = basegfx::B2IVector( aOrigin.X, aOrigin.Y ); + + ///add a scaling independent Offset if requested + if( nScreenValueOffsetInRadiusDirection != 0) + { + basegfx::B2IVector aDirection( aScreenPosition2D.X- aOrigin.X, aScreenPosition2D.Y- aOrigin.Y ); + aDirection.setLength(nScreenValueOffsetInRadiusDirection); + aScreenPosition2D.X += aDirection.getX(); + aScreenPosition2D.Y += aDirection.getY(); + } + + // compute outer pie radius + awt::Point aOuterCirclePoint = PlottingPositionHelper::transformSceneToScreenPosition( + m_pPosHelper->transformUnitCircleToScene( + 0, + rParam.mfUnitCircleOuterRadius, + 0 ), + m_xLogicTarget, m_nDimension ); + basegfx::B2IVector aRadiusVector( + aOuterCirclePoint.X - aPieLabelInfo.aOrigin.getX(), + aOuterCirclePoint.Y - aPieLabelInfo.aOrigin.getY() ); + double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector); + double fPieRadius = sqrt( fSquaredPieRadius ); + double fAngleDegree + = rParam.mfUnitCircleStartAngleDegree + rParam.mfUnitCircleWidthAngleDegree / 2.0; + while (fAngleDegree > 360.0) + fAngleDegree -= 360.0; + while (fAngleDegree < 0.0) + fAngleDegree += 360.0; + + awt::Point aOuterPosition = PlottingPositionHelper::transformSceneToScreenPosition( + m_pPosHelper->transformUnitCircleToScene(fAngleDegree, rParam.mfUnitCircleOuterRadius, 0), + m_xLogicTarget, m_nDimension); + aPieLabelInfo.aOuterPosition = basegfx::B2IVector(aOuterPosition.X, aOuterPosition.Y); + + // set the maximum text width to be used when text wrapping is enabled + double fTextMaximumFrameWidth = 0.8 * fPieRadius; + if (nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE + && m_aAvailableOuterRect.getWidth()) + { + if ((fAngleDegree >= 67.5 && fAngleDegree <= 112.5) + || (fAngleDegree >= 247.5 && fAngleDegree <= 292.5)) + fTextMaximumFrameWidth = m_aAvailableOuterRect.getWidth() / 3.0; + else + fTextMaximumFrameWidth = 0.85 * (m_aAvailableOuterRect.getWidth() / 2.0 - fPieRadius); + } + sal_Int32 nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth); + + ///the text shape for the label is created + aPieLabelInfo.xTextShape = createDataLabel( + xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum, + aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth); + + ///a new `PieLabelInfo` instance is initialized with all the info related to + ///the current label in order to simplify later label position rearrangement; + rtl::Reference< SvxShape > xChild = aPieLabelInfo.xTextShape; + + ///text shape could be empty; in that case there is no need to add label info + if( !xChild.is() ) + return; + + aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get()); + + if (bMovementAllowed && !m_bUseRings) + { + /** Handle the placement of the label in the best fit case. + * First off the routine try to place the label inside the related pie slice, + * if this is not possible the label is placed outside. + */ + if (rSeries.getLabelPlacement(nPointIndex, m_xChartTypeModel, m_pPosHelper->isSwapXAndY()) + == css::chart::DataLabelPlacement::CUSTOM + || !performLabelBestFitInnerPlacement(rParam, aPieLabelInfo)) + { + if (m_aAvailableOuterRect.getWidth()) + { + if ((fAngleDegree >= 67.5 && fAngleDegree <= 112.5) + || (fAngleDegree >= 247.5 && fAngleDegree <= 292.5)) + fTextMaximumFrameWidth = m_aAvailableOuterRect.getWidth() / 3.0; + else + fTextMaximumFrameWidth + = 0.85 * (m_aAvailableOuterRect.getWidth() / 2.0 - fPieRadius); + nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth); + } + + nScreenValueOffsetInRadiusDirection = (m_nDimension != 3) ? 150 : 0; + aScreenPosition2D + = aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues( + eAlignment, css::chart::DataLabelPlacement::OUTSIDE, + rParam.mfUnitCircleStartAngleDegree, + rParam.mfUnitCircleWidthAngleDegree, rParam.mfUnitCircleInnerRadius, + rParam.mfUnitCircleOuterRadius, rParam.mfLogicZ + 0.5, 0); + aPieLabelInfo.aFirstPosition + = basegfx::B2IVector(aScreenPosition2D.X, aScreenPosition2D.Y); + + //add a scaling independent Offset if requested + if (nScreenValueOffsetInRadiusDirection != 0) + { + basegfx::B2IVector aDirection(aScreenPosition2D.X - aOrigin.X, + aScreenPosition2D.Y - aOrigin.Y); + aDirection.setLength(nScreenValueOffsetInRadiusDirection); + aScreenPosition2D.X += aDirection.getX(); + aScreenPosition2D.Y += aDirection.getY(); + } + + uno::Reference<drawing::XShapes> xShapes(xChild->getParent(), uno::UNO_QUERY); + xShapes->remove(aPieLabelInfo.xTextShape); + aPieLabelInfo.xTextShape + = createDataLabel(xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum, + aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth); + xChild = aPieLabelInfo.xTextShape; + if (!xChild.is()) + return; + + aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get()); + } + } + + bool bShowLeaderLine = rSeries.getPropertiesOfSeries() + ->getPropertyValue("ShowCustomLeaderLines") + .get<sal_Bool>(); + if (m_bPieLabelsAllowToMove) + { + ::basegfx::B2IRectangle aRect(lcl_getRect(aPieLabelInfo.xLabelGroupShape)); + sal_Int32 nPageWidth = m_aPageReferenceSize.Width; + sal_Int32 nPageHeight = m_aPageReferenceSize.Height; + + // the data label should be inside the chart area + awt::Point aShapePos = aPieLabelInfo.xLabelGroupShape->getPosition(); + if (aRect.getMinX() < 0) + aPieLabelInfo.xLabelGroupShape->setPosition( + awt::Point(aShapePos.X - aRect.getMinX(), aShapePos.Y)); + if (aRect.getMinY() < 0) + aPieLabelInfo.xLabelGroupShape->setPosition( + awt::Point(aShapePos.X, aShapePos.Y - aRect.getMinY())); + if (aRect.getMaxX() > nPageWidth) + aPieLabelInfo.xLabelGroupShape->setPosition( + awt::Point(aShapePos.X - (aRect.getMaxX() - nPageWidth), aShapePos.Y)); + if (aRect.getMaxY() > nPageHeight) + aPieLabelInfo.xLabelGroupShape->setPosition( + awt::Point(aShapePos.X, aShapePos.Y - (aRect.getMaxY() - nPageHeight))); + + if (rSeries.isLabelCustomPos(nPointIndex) && bShowLeaderLine) + { + sal_Int32 nX1 = aPieLabelInfo.aOuterPosition.getX(); + sal_Int32 nY1 = aPieLabelInfo.aOuterPosition.getY(); + sal_Int32 nX2 = nX1; + sal_Int32 nY2 = nY1; + if (nX1 < aRect.getMinX()) + nX2 = aRect.getMinX(); + else if (nX1 > aRect.getMaxX()) + nX2 = aRect.getMaxX(); + + if (nY1 < aRect.getMinY()) + nY2 = aRect.getMinY(); + else if (nY1 > aRect.getMaxY()) + nY2 = aRect.getMaxY(); + + sal_Int32 nSquaredDistanceFromOrigin + = (nX2 - aOrigin.X) * (nX2 - aOrigin.X) + (nY2 - aOrigin.Y) * (nY2 - aOrigin.Y); + + // tdf#138018 Don't show leader line when custom positioned data label is inside pie chart + if (nSquaredDistanceFromOrigin > fSquaredPieRadius) + { + //when the line is very short compared to the page size don't create one + ::basegfx::B2DVector aLength(nX1 - nX2, nY1 - nY2); + double fPageDiagonaleLength = std::hypot(nPageWidth, nPageHeight); + if ((aLength.getLength() / fPageDiagonaleLength) >= 0.01) + { + drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } }; + + VLineProperties aVLineProperties; + if (aPieLabelInfo.xTextShape.is()) + { + sal_Int32 nColor = 0; + aPieLabelInfo.xTextShape->SvxShape::getPropertyValue("CharColor") >>= nColor; + //automatic font color does not work for lines -> fallback to black + if (nColor != -1) + aVLineProperties.Color <<= nColor; + } + ShapeFactory::createLine2D(xTextTarget, aPoints, &aVLineProperties); + } + } + } + } + + aPieLabelInfo.fValue = nVal; + aPieLabelInfo.bMovementAllowed = bMovementAllowed; + aPieLabelInfo.bMoved = false; + aPieLabelInfo.xTextTarget = xTextTarget; + aPieLabelInfo.bShowLeaderLine = bShowLeaderLine && !rSeries.isLabelCustomPos(nPointIndex); + + m_aLabelInfoList.push_back(aPieLabelInfo); +} + +void PieChart::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 /* zSlot */, sal_Int32 /* xSlot */, sal_Int32 /* ySlot */ ) +{ + VSeriesPlotter::addSeries( std::move(pSeries), 0, -1, 0 ); +} + +double PieChart::getMinimumX() +{ + return 0.5; +} +double PieChart::getMaxOffset() +{ + if (!std::isnan(m_fMaxOffset)) + // Value already cached. Use it. + return m_fMaxOffset; + + m_fMaxOffset = 0.0; + if( m_aZSlots.empty() ) + return m_fMaxOffset; + if( m_aZSlots.front().empty() ) + return m_fMaxOffset; + + const std::vector< std::unique_ptr<VDataSeries> >& rSeriesList( m_aZSlots.front().front().m_aSeriesVector ); + if(rSeriesList.empty()) + return m_fMaxOffset; + + VDataSeries* pSeries = rSeriesList.front().get(); + uno::Reference< beans::XPropertySet > xSeriesProp( pSeries->getPropertiesOfSeries() ); + if( !xSeriesProp.is() ) + return m_fMaxOffset; + + double fExplodePercentage=0.0; + xSeriesProp->getPropertyValue( "Offset") >>= fExplodePercentage; + if(fExplodePercentage>m_fMaxOffset) + m_fMaxOffset=fExplodePercentage; + + if(!m_bSizeExcludesLabelsAndExplodedSegments) + { + uno::Sequence< sal_Int32 > aAttributedDataPointIndexList; + if( xSeriesProp->getPropertyValue( "AttributedDataPoints" ) >>= aAttributedDataPointIndexList ) + { + for(sal_Int32 nN=aAttributedDataPointIndexList.getLength();nN--;) + { + uno::Reference< beans::XPropertySet > xPointProp( pSeries->getPropertiesOfPoint(aAttributedDataPointIndexList[nN]) ); + if(xPointProp.is()) + { + fExplodePercentage=0.0; + xPointProp->getPropertyValue( "Offset") >>= fExplodePercentage; + if(fExplodePercentage>m_fMaxOffset) + m_fMaxOffset=fExplodePercentage; + } + } + } + } + return m_fMaxOffset; +} +double PieChart::getMaximumX() +{ + double fMaxOffset = getMaxOffset(); + if( !m_aZSlots.empty() && m_bUseRings) + return m_aZSlots.front().size()+0.5+fMaxOffset; + return 1.5+fMaxOffset; +} +double PieChart::getMinimumYInRange( double /* fMinimumX */, double /* fMaximumX */, sal_Int32 /* nAxisIndex */ ) +{ + return 0.0; +} + +double PieChart::getMaximumYInRange( double /* fMinimumX */, double /* fMaximumX */, sal_Int32 /* nAxisIndex */ ) +{ + return 1.0; +} + +bool PieChart::isExpandBorderToIncrementRhythm( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +bool PieChart::isExpandIfValuesCloseToBorder( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +bool PieChart::isExpandWideValuesToZero( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +bool PieChart::isExpandNarrowValuesTowardZero( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +bool PieChart::isSeparateStackingForDifferentSigns( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +void PieChart::createShapes() +{ + ///a ZSlot is a vector< vector< VDataSeriesGroup > >. There is only one + ///ZSlot: m_aZSlots[0] which has a number of elements equal to the total + ///number of data series (in fact, even if m_aZSlots[0][i] is an object of + ///type `VDataSeriesGroup`, in the current implementation, there is only one + ///data series in each data series group). + if (m_aZSlots.empty()) + // No series to plot. + return; + + ///m_xLogicTarget is where the group of all data series shapes (e.g. a pie + ///slice) is added (xSeriesTarget); + + ///m_xFinalTarget is where the group of all text shapes (labels) is added + ///(xTextTarget). + + ///both have been already created and added to the same root shape + ///( a member of a VDiagram object); this initialization occurs in + ///`ChartView::impl_createDiagramAndContent`. + + OSL_ENSURE(m_xLogicTarget.is() && m_xFinalTarget.is(), "PieChart is not properly initialized."); + if (!m_xLogicTarget.is() || !m_xFinalTarget.is()) + return; + + ///the text labels should be always on top of the other series shapes + ///therefore create an own group for the texts to move them to front + ///(because the text group is created after the series group the texts are + ///displayed on top) + rtl::Reference<SvxShapeGroupAnyD> xSeriesTarget = createGroupShape( m_xLogicTarget ); + rtl::Reference<SvxShapeGroup> xTextTarget = ShapeFactory::createGroup2D( m_xFinalTarget ); + //check necessary here that different Y axis can not be stacked in the same group? ... hm? + + ///pay attention that the `m_bSwapXAndY` parameter used by the polar + ///plotting position helper is always set to true for pie/donut charts + ///(see PieChart::setScales). This fact causes that `createShapes` expects + ///that the radius axis scale is the one with index 0 and the angle axis + ///scale is the one with index 1. + + std::vector< VDataSeriesGroup >::iterator aXSlotIter = m_aZSlots.front().begin(); + const std::vector< VDataSeriesGroup >::const_iterator aXSlotEnd = m_aZSlots.front().end(); + + ///m_bUseRings == true if chart type is `donut`, == false if chart type is + ///`pie`; if the chart is of `donut` type we have as many rings as many data + ///series, else we have a single ring (a pie) representing the first data + ///series; + ///for what I can see the radius axis orientation is always reversed and + ///the angle axis orientation is always non-reversed; + ///the radius axis scale range is [0.5, number of rings + 0.5 + max_offset], + ///the angle axis scale range is [0, 1]. The max_offset parameter is used + ///for exploded pie chart and its value is 0.5. + + ///the `explodeable` ring is the first one except when the radius axis + ///orientation is reversed (always!?) and we are dealing with a donut: in + ///such a case the `explodeable` ring is the last one. + std::vector< VDataSeriesGroup >::size_type nExplodeableSlot = 0; + if( m_pPosHelper->isMathematicalOrientationRadius() && m_bUseRings ) + nExplodeableSlot = m_aZSlots.front().size()-1; + + m_aLabelInfoList.clear(); + m_fMaxOffset = std::numeric_limits<double>::quiet_NaN(); + sal_Int32 n3DRelativeHeight = 100; + if ( (m_nDimension==3) && m_xChartTypeModel.is()) + { + try + { + uno::Any aAny = m_xChartTypeModel->getPropertyValue( "3DRelativeHeight" ); + aAny >>= n3DRelativeHeight; + } + catch (const uno::Exception&) { } + } + ///iterate over each xslot, that is on each data series (there is + ///only one data series in each data series group!); note that if the chart + ///type is a pie the loop iterates only over the first data series + ///(m_bUseRings||fSlotX<0.5) + for( double fSlotX=0; aXSlotIter != aXSlotEnd && (m_bUseRings||fSlotX<0.5 ); ++aXSlotIter, fSlotX+=1.0 ) + { + ShapeParam aParam; + + std::vector< std::unique_ptr<VDataSeries> >* pSeriesList = &(aXSlotIter->m_aSeriesVector); + if(pSeriesList->empty())//there should be only one series in each x slot + continue; + VDataSeries* pSeries = pSeriesList->front().get(); + if(!pSeries) + continue; + + bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor"); + + /// The angle degree offset is set by the same property of the + /// data series. + /// Counter-clockwise offset from the 3 o'clock position. + m_pPosHelper->m_fAngleDegreeOffset = pSeries->getStartingAngle(); + + ///iterate through all points to get the sum of all entries of + ///the current data series + sal_Int32 nPointIndex=0; + sal_Int32 nPointCount=pSeries->getTotalPointCount(); + for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ ) + { + double fY = pSeries->getYValue( nPointIndex ); + if(fY<0.0) + { + //@todo warn somehow that negative values are treated as positive + } + if( std::isnan(fY) ) + continue; + aParam.mfLogicYSum += fabs(fY); + } + + if (aParam.mfLogicYSum == 0.0) + // Total sum of all Y values in this series is zero. Skip the whole series. + continue; + + double fLogicYForNextPoint = 0.0; + ///iterate through all points to create shapes + for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ ) + { + double fLogicInnerRadius, fLogicOuterRadius; + + ///compute the maximum relative distance offset of the current slice + ///from the pie center + ///it is worth noting that after the first invocation the maximum + ///offset value is cached, so it is evaluated only once per each + ///call to `createShapes` + double fOffset = getMaxOffset(); + + ///compute the outer and the inner radius for the current ring slice + bool bIsVisible = m_pPosHelper->getInnerAndOuterRadius( fSlotX+1.0, fLogicInnerRadius, fLogicOuterRadius, m_bUseRings, fOffset ); + if( !bIsVisible ) + continue; + + aParam.mfDepth = getTransformedDepth() * (n3DRelativeHeight / 100.0); + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries, xSeriesTarget); + ///collect data point information (logic coordinates, style ): + double fLogicYValue = fabs(pSeries->getYValue( nPointIndex )); + if( std::isnan(fLogicYValue) ) + continue; + if(fLogicYValue==0.0)//@todo: continue also if the resolution is too small + continue; + double fLogicYPos = fLogicYForNextPoint; + fLogicYForNextPoint += fLogicYValue; + + uno::Reference< beans::XPropertySet > xPointProperties = pSeries->getPropertiesOfPoint( nPointIndex ); + + //iterate through all subsystems to create partial points + { + //logic values on angle axis: + double fLogicStartAngleValue = fLogicYPos / aParam.mfLogicYSum; + double fLogicEndAngleValue = (fLogicYPos+fLogicYValue) / aParam.mfLogicYSum; + + ///note that the explode percentage is set to the `Offset` + ///property of the current data series entry only for slices + ///belonging to the outer ring + aParam.mfExplodePercentage = 0.0; + bool bDoExplode = ( nExplodeableSlot == static_cast< std::vector< VDataSeriesGroup >::size_type >(fSlotX) ); + if(bDoExplode) try + { + xPointProperties->getPropertyValue( "Offset") >>= aParam.mfExplodePercentage; + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + ///see notes for `PolarPlottingPositionHelper` methods + ///transform to unit circle: + aParam.mfUnitCircleWidthAngleDegree = m_pPosHelper->getWidthAngleDegree( fLogicStartAngleValue, fLogicEndAngleValue ); + aParam.mfUnitCircleStartAngleDegree = m_pPosHelper->transformToAngleDegree( fLogicStartAngleValue ); + aParam.mfUnitCircleInnerRadius = m_pPosHelper->transformToRadius( fLogicInnerRadius ); + aParam.mfUnitCircleOuterRadius = m_pPosHelper->transformToRadius( fLogicOuterRadius ); + + ///create data point + aParam.mfLogicZ = -1.0; // For 3D pie chart label position + + // Do concentric explosion if it's a donut chart with more than one series + const bool bConcentricExplosion = m_bUseRings && (m_aZSlots.front().size() > 1); + rtl::Reference<SvxShape> xPointShape = + createDataPoint( + xSeriesGroupShape_Shapes, xPointProperties, aParam, nPointCount, + bConcentricExplosion); + + ///point color: + if (!pSeries->hasPointOwnColor(nPointIndex) && m_xColorScheme.is()) + { + xPointShape->setPropertyValue("FillColor", + uno::Any(m_xColorScheme->getColorByIndex( nPointIndex ))); + } + + + if(bHasFillColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nPointIndex, "FillColor"); + if(!std::isnan(nPropVal)) + { + xPointShape->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>( nPropVal))); + } + } + + ///create label + createTextLabelShape(xTextTarget, *pSeries, nPointIndex, aParam); + + if(!bDoExplode) + { + ShapeFactory::setShapeName( xPointShape + , ObjectIdentifier::createPointCID( pSeries->getPointCID_Stub(), nPointIndex ) ); + } + else try + { + ///enable dragging of outer segments + + double fAngle = aParam.mfUnitCircleStartAngleDegree + aParam.mfUnitCircleWidthAngleDegree/2.0; + double fMaxDeltaRadius = aParam.mfUnitCircleOuterRadius-aParam.mfUnitCircleInnerRadius; + drawing::Position3D aOrigin = m_pPosHelper->transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius, aParam.mfLogicZ ); + drawing::Position3D aNewOrigin = m_pPosHelper->transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius + fMaxDeltaRadius, aParam.mfLogicZ ); + + sal_Int32 nOffsetPercent( static_cast<sal_Int32>(aParam.mfExplodePercentage * 100.0) ); + + awt::Point aMinimumPosition( PlottingPositionHelper::transformSceneToScreenPosition( + aOrigin, m_xLogicTarget, m_nDimension ) ); + awt::Point aMaximumPosition( PlottingPositionHelper::transformSceneToScreenPosition( + aNewOrigin, m_xLogicTarget, m_nDimension ) ); + + //enable dragging of piesegments + OUString aPointCIDStub( ObjectIdentifier::createSeriesSubObjectStub( OBJECTTYPE_DATA_POINT + , pSeries->getSeriesParticle() + , ObjectIdentifier::getPieSegmentDragMethodServiceName() + , ObjectIdentifier::createPieSegmentDragParameterString( + nOffsetPercent, aMinimumPosition, aMaximumPosition ) + ) ); + + ShapeFactory::setShapeName( xPointShape + , ObjectIdentifier::createPointCID( aPointCIDStub, nPointIndex ) ); + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + }//next series in x slot (next y slot) + }//next category + }//next x slot +} + +PieChart::PieLabelInfo::PieLabelInfo() + : fValue(0.0) + , bMovementAllowed(false), bMoved(false) + , bShowLeaderLine(false), pPrevious(nullptr) + , pNext(nullptr) +{ +} + +/** In case this label and the passed label overlap the routine moves this + * label in order to fix the issue. After the label position has been + * rearranged it is checked that the moved label is still inside the page + * document, if the test is positive the routine returns true else returns + * false. + */ +bool PieChart::PieLabelInfo::moveAwayFrom( const PieChart::PieLabelInfo* pFix, const awt::Size& rPageSize, bool bMoveHalfWay, bool bMoveClockwise ) +{ + //return true if the move was successful + if(!bMovementAllowed) + return false; + + const sal_Int32 nLabelDistanceX = rPageSize.Width/50; + const sal_Int32 nLabelDistanceY = rPageSize.Height/50; + + ///compute the rectangle representing the intersection of the label bounding + ///boxes (`aOverlap`). + ::basegfx::B2IRectangle aOverlap( lcl_getRect( xLabelGroupShape ) ); + aOverlap.intersect( lcl_getRect( pFix->xLabelGroupShape ) ); + if( aOverlap.isEmpty() ) + return true; + + //TODO: alternative move direction + + ///the label is shifted along the direction orthogonal to the vector + ///starting at the pie/donut center and ending at this label anchor + ///point; + + ///named `aTangentialDirection` the unit vector related to such a + ///direction, the magnitude of the shift along such a direction is + ///calculated in this way: if the horizontal component of + ///`aTangentialDirection` is greater than the vertical component, + ///the magnitude of the shift is equal to `aOverlap.Width` else to + ///`aOverlap.Height`; + basegfx::B2IVector aRadiusDirection = aFirstPosition - aOrigin; + aRadiusDirection.setLength(1.0); + basegfx::B2IVector aTangentialDirection( -aRadiusDirection.getY(), aRadiusDirection.getX() ); + bool bShiftHorizontal = abs(aTangentialDirection.getX()) > abs(aTangentialDirection.getY()); + sal_Int32 nShift = bShiftHorizontal ? static_cast<sal_Int32>(aOverlap.getWidth()) : static_cast<sal_Int32>(aOverlap.getHeight()); + ///the magnitude of the shift is also increased by 1/50-th of the width + ///or the height of the document page; + nShift += (bShiftHorizontal ? nLabelDistanceX : nLabelDistanceY); + ///in case the `bMoveHalfWay` parameter is true the magnitude of + ///the shift is halved. + if( bMoveHalfWay ) + nShift/=2; + ///in case the `bMoveClockwise` parameter is false the direction of + ///`aTangentialDirection` is reversed; + if(!bMoveClockwise) + nShift*=-1; + awt::Point aOldPos( xLabelGroupShape->getPosition() ); + basegfx::B2IVector aNewPos = basegfx::B2IVector( aOldPos.X, aOldPos.Y ) + nShift*aTangentialDirection; + + ///a final check is performed in order to be sure that the moved label + ///is still inside the page document; + awt::Point aNewAWTPos( aNewPos.getX(), aNewPos.getY() ); + if( !lcl_isInsidePage( aNewAWTPos, xLabelGroupShape->getSize(), rPageSize ) ) + return false; + + xLabelGroupShape->setPosition( aNewAWTPos ); + bMoved = true; + + return true; + + ///note that no further test is performed in order to check that the + ///overlap is really fixed: this result is surely achieved if the shift + ///would occur in the horizontal or vertical direction (since, in such a + ///direction, the magnitude of the shift would be greater than the length + ///of the overlap), but in general this is not true; + ///adding a constant term equal to 1/50-th of the width or the height of + ///the document page increases the probability of success, anyway it is + ///worth noting that the method can return true even if the overlap issue + ///is not (completely) fixed; +} + +void PieChart::resetLabelPositionsToPreviousState() +{ + for (auto const& labelInfo : m_aLabelInfoList) + labelInfo.xLabelGroupShape->setPosition(labelInfo.aPreviousPosition); +} + +bool PieChart::detectLabelOverlapsAndMove( const awt::Size& rPageSize ) +{ + ///the routine tries to individuate a chain of overlapping labels and + ///assigns the first and the last of them to `pFirstBorder` and + ///`pSecondBorder`; + ///this result is achieved by performing two consecutive while loop. + + ///find borders of a group of overlapping labels + + ///a first while loop is started on the collection of `PieLabelInfo` objects; + ///the bounding box of each label is checked for overlap against the bounding + ///box of the previous and of the next label; + ///when an overlap is found `bOverlapFound` is set to true, however the + ///iteration is break only if the overlap occurs against only the next label + ///and not against the previous label: so we exit from the loop whenever an + ///overlap occurs except when the loop initial label overlaps with the + ///previous one; + bool bOverlapFound = false; + PieLabelInfo* pStart = &(*(m_aLabelInfoList.rbegin())); + PieLabelInfo* pFirstBorder = nullptr; + PieLabelInfo* pSecondBorder = nullptr; + PieLabelInfo* pCurrent = pStart; + do + { + ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) ); + ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap ); + aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) ); + aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) ); + + bool bPreviousOverlap = !aPreviousOverlap.isEmpty(); + bool bNextOverlap = !aNextOverlap.isEmpty(); + if( bPreviousOverlap || bNextOverlap ) + bOverlapFound = true; + if( !bPreviousOverlap && bNextOverlap ) + { + pFirstBorder = pCurrent; + break; + } + pCurrent = pCurrent->pNext; + } + while( pCurrent != pStart ); + + if( !bOverlapFound ) + return false; + + ///in case we found a label (`pFirstBorder`) which overlaps with the next + ///label and not with the previous label a second while loop is started with + ///`pFirstBorder` as initial label; one more time the bounding box of each + ///label is checked for overlap against the bounding box of the previous and + ///of the next label, however this time we exit from the loop only if the + ///current label overlaps with the previous one but does not with the next + ///one (the opposite of what is required in the former loop); + ///in case such a label is found it is assigned to `pSecondBorder` and the + ///iteration is stopped; so in case there is a chain of overlapping labels + ///we end up having the first label of the chain pointed by `pFirstBorder` + ///and the last label of the chain pointed by `pSecondBorder`; + if( pFirstBorder ) + { + pCurrent = pFirstBorder; + do + { + ::basegfx::B2IRectangle aPreviousOverlap( lcl_getRect( pCurrent->xLabelGroupShape ) ); + ::basegfx::B2IRectangle aNextOverlap( aPreviousOverlap ); + aPreviousOverlap.intersect( lcl_getRect( pCurrent->pPrevious->xLabelGroupShape ) ); + aNextOverlap.intersect( lcl_getRect( pCurrent->pNext->xLabelGroupShape ) ); + + if( !aPreviousOverlap.isEmpty() && aNextOverlap.isEmpty() ) + { + pSecondBorder = pCurrent; + break; + } + pCurrent = pCurrent->pNext; + } + while( pCurrent != pFirstBorder ); + } + + ///when two labels satisfying the required conditions are not found + ///(`pFirstBorder == 0 || pSecondBorder == 0`) but still an overlap occurs + ///(`bOverlapFound == true`) we are in the situation where each label + ///overlaps with both the previous and the next one; so `pFirstBorder` is + ///set to point to the last `PieLabelInfo` object in the collection and + ///`pSecondBorder` is set to point to the first one; + if( !pFirstBorder || !pSecondBorder ) + { + pFirstBorder = &(*(m_aLabelInfoList.rbegin())); + pSecondBorder = &(*(m_aLabelInfoList.begin())); + } + + ///the total number of labels that made up the chain is calculated and used + ///for getting a pointer to the central label (`pCenter`); + PieLabelInfo* pCenter = pFirstBorder; + sal_Int32 nOverlapGroupCount = 1; + for( pCurrent = pFirstBorder ;pCurrent != pSecondBorder; pCurrent = pCurrent->pNext ) + nOverlapGroupCount++; + sal_Int32 nCenterPos = nOverlapGroupCount/2; + bool bSingleCenter = nOverlapGroupCount%2 != 0; + if( bSingleCenter ) + nCenterPos++; + if(nCenterPos>1) + { + pCurrent = pFirstBorder; + while( --nCenterPos ) + pCurrent = pCurrent->pNext; + pCenter = pCurrent; + } + + ///the current position of each label in the collection is saved in + ///`PieLabelInfo.aPreviousPosition`, so that it is possible to undo the label + ///move action if it is needed; the undo action is provided by the + ///`PieChart::resetLabelPositionsToPreviousState` method. + pCurrent = pStart; + do + { + pCurrent->aPreviousPosition = pCurrent->xLabelGroupShape->getPosition(); + pCurrent = pCurrent->pNext; + } + while( pCurrent != pStart ); + + ///the `PieChart::tryMoveLabels` method is invoked with + ///`rbAlternativeMoveDirection` boolean parameter set to false, such a method + ///tries to remove all overlaps that occur in the list of labels going from + ///`pFirstBorder` to `pSecondBorder`; + ///if the `PieChart::tryMoveLabels` returns true no further action is + ///performed, however it is worth noting that it does not mean that all + ///overlap issues have been surely fixed, but only that all moved labels are + ///at least completely inside the page document; + ///when `PieChart::tryMoveLabels` returns false, it means that the attempt + ///to fix one of the overlap issues caused that a label has been moved + ///(partially) outside the page document (anyway the `PieChart::tryMoveLabels` + ///method takes care to restore the position of all labels to their initial + ///position, and to set the `rbAlternativeMoveDirection` in/out parameter to + ///true); in such a case a second invocation of `PieChart::tryMoveLabels` is + ///performed (and this time the `rbAlternativeMoveDirection` boolean + ///parameter is true) and independently by what the `PieChart::tryMoveLabels` + ///method returns no further action is performed; + ///(see notes for `PieChart::tryMoveLabels`); + bool bAlternativeMoveDirection = false; + if( !tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize ) ) + tryMoveLabels( pFirstBorder, pSecondBorder, pCenter, bSingleCenter, bAlternativeMoveDirection, rPageSize ); + + ///in both cases (one or two invocations of `PieChart::tryMoveLabels`) the + ///`detectLabelOverlapsAndMove` method ends returning true. + return true; +} + + +/** Try to remove all overlaps that occur in the list of labels going from + * `pFirstBorder` to `pSecondBorder` + */ +bool PieChart::tryMoveLabels( PieLabelInfo const * pFirstBorder, PieLabelInfo const * pSecondBorder + , PieLabelInfo* pCenter + , bool bSingleCenter, bool& rbAlternativeMoveDirection, const awt::Size& rPageSize ) +{ + + PieLabelInfo* p1 = bSingleCenter ? pCenter->pPrevious : pCenter; + PieLabelInfo* p2 = pCenter->pNext; + //return true when successful + + bool bLabelOrderIsAntiClockWise = m_pPosHelper->isMathematicalOrientationAngle(); + + ///two loops are performed simultaneously: the outer loop iterates on + ///`PieLabelInfo` objects in the list starting from the central element + ///(`pCenter`) and moving forward until the last element (`pSecondBorder`); + ///the inner loop starts from the previous element of `pCenter` and moves + ///forward until the current `PieLabelInfo` object of the outer loop is + ///reached + PieLabelInfo* pCurrent = nullptr; + for( pCurrent = p2 ;pCurrent->pPrevious != pSecondBorder; pCurrent = pCurrent->pNext ) + { + PieLabelInfo* pFix = nullptr; + for( pFix = p2->pPrevious ;pFix != pCurrent; pFix = pFix->pNext ) + { + ///on the current `PieLabelInfo` object of the outer loop the + ///`moveAwayFrom` method is invoked by passing the current + ///`PieLabelInfo` object of the inner loop as argument. + + ///so each label going from the central one to the last one is + ///checked for overlapping against all previous labels (that comes + ///after the central label) and in case the overlap occurs the + ///`moveAwayFrom` method tries to fix the issue; + ///if `moveAwayFrom` returns true (pay attention: that does not + ///mean that the overlap issue has been surely fixed but only that + ///the moved label is at least completely inside the page document: + ///see notes on `PieChart::PieLabelInfo::moveAwayFrom`), the inner + ///loop starts a new iteration else the `rbAlternativeMoveDirection` + ///boolean parameter is tested: if it is false the parameter is set + ///to true, the position of all labels is restored to the initial + ///one (through the `PieChart::resetLabelPositionsToPreviousState` + ///method) and the method ends by returning false, else the inner + ///loop starts a new iteration step; + ///so when `rbAlternativeMoveDirection` is true the method goes on + ///trying to fix left overlap issues even if the last `moveAwayFrom` + ///invocation has moved a label in a position that it is not + ///completely inside the page document + + if( !pCurrent->moveAwayFrom( pFix, rPageSize, !bSingleCenter && pCurrent == p2, !bLabelOrderIsAntiClockWise ) ) + { + if( !rbAlternativeMoveDirection ) + { + rbAlternativeMoveDirection = true; + resetLabelPositionsToPreviousState(); + return false; + } + } + } + } + + ///if the method does not return before ending the first pair of loops, + ///a second pair of simultaneous loops is performed in the opposite + ///direction (respect with the previous case): the outer loop iterates on + ///`PieLabelInfo` objects in the list starting from the central element + ///(`pCenter`) and moving backward until the first element (`pFirstBorder`); + ///the inner loop starts from the next element of `pCenter` and moves + ///backward until the current `PieLabelInfo` object of the outer loop is + ///reached + + ///like in the previous case on the current `PieLabelInfo` object of + ///the outer loop the `moveAwayFrom` method is invoked by passing + ///the current `PieLabelInfo` object of the inner loop as argument + + ///so each label going from the central one to the first one is checked for + ///overlapping on all subsequent labels (that come before the central label) + ///and in case the overlap occurs the `moveAwayFrom` method tries to fix + ///the issue. The subsequent actions performed after the invocation + ///`moveAwayFrom` are the same detailed above for the first pair of loops + + for( pCurrent = p1 ;pCurrent->pNext != pFirstBorder; pCurrent = pCurrent->pPrevious ) + { + PieLabelInfo* pFix = nullptr; + for( pFix = p2->pNext ;pFix != pCurrent; pFix = pFix->pPrevious ) + { + if( !pCurrent->moveAwayFrom( pFix, rPageSize, false, bLabelOrderIsAntiClockWise ) ) + { + if( !rbAlternativeMoveDirection ) + { + rbAlternativeMoveDirection = true; + resetLabelPositionsToPreviousState(); + return false; + } + } + } + } + return true; +} + +void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSize ) +{ + ///this method is invoked by `ChartView::impl_createDiagramAndContent` for + ///pie and donut charts after text label creation; + ///it tries to rearrange labels only when the label placement type is + ///`AVOID_OVERLAP`. + // no need to do anything when we only have one label + if (m_aLabelInfoList.size() < 2) + return; + + ///check whether there are any labels that should be moved + bool bMoveableFound = false; + for (auto const& labelInfo : m_aLabelInfoList) + { + if(labelInfo.bMovementAllowed) + { + bMoveableFound = true; + break; + } + } + if(!bMoveableFound) + return; + + double fPageDiagonaleLength = std::hypot(rPageSize.Width, rPageSize.Height); + if( fPageDiagonaleLength == 0.0 ) + return; + + ///initialize next and previous member of `PieLabelInfo` objects + auto aIt1 = m_aLabelInfoList.begin(); + auto aEnd = m_aLabelInfoList.end(); + std::vector< PieLabelInfo >::iterator aIt2 = aIt1; + aIt1->pPrevious = &(*(m_aLabelInfoList.rbegin())); + ++aIt2; + for( ;aIt2!=aEnd; ++aIt1, ++aIt2 ) + { + PieLabelInfo& rInfo1( *aIt1 ); + PieLabelInfo& rInfo2( *aIt2 ); + rInfo1.pNext = &rInfo2; + rInfo2.pPrevious = &rInfo1; + } + aIt1->pNext = &(*(m_aLabelInfoList.begin())); + + ///detect overlaps and move + sal_Int32 nMaxIterations = 50; + while( detectLabelOverlapsAndMove( rPageSize ) && nMaxIterations > 0 ) + nMaxIterations--; + + ///create connection lines for the moved labels + VLineProperties aVLineProperties; + for (auto const& labelInfo : m_aLabelInfoList) + { + if( labelInfo.bMoved && labelInfo.bShowLeaderLine ) + { + sal_Int32 nX1 = labelInfo.aOuterPosition.getX(); + sal_Int32 nY1 = labelInfo.aOuterPosition.getY(); + sal_Int32 nX2 = nX1; + sal_Int32 nY2 = nY1; + ::basegfx::B2IRectangle aRect( lcl_getRect( labelInfo.xLabelGroupShape ) ); + if( nX1 < aRect.getMinX() ) + nX2 = aRect.getMinX(); + else if( nX1 > aRect.getMaxX() ) + nX2 = aRect.getMaxX(); + + if( nY1 < aRect.getMinY() ) + nY2 = aRect.getMinY(); + else if( nY1 > aRect.getMaxY() ) + nY2 = aRect.getMaxY(); + + //when the line is very short compared to the page size don't create one + ::basegfx::B2DVector aLength(nX1-nX2, nY1-nY2); + if( (aLength.getLength()/fPageDiagonaleLength) < 0.01 ) + continue; + + drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } }; + + if( labelInfo.xTextShape.is() ) + { + sal_Int32 nColor = 0; + labelInfo.xTextShape->SvxShape::getPropertyValue("CharColor") >>= nColor; + if( nColor != -1 )//automatic font color does not work for lines -> fallback to black + aVLineProperties.Color <<= nColor; + } + ShapeFactory::createLine2D( labelInfo.xTextTarget, aPoints, &aVLineProperties ); + } + } +} + + +/** Handle the placement of the label in the best fit case: + * the routine try to place the label inside the related pie slice, + * in case of success it returns true else returns false. + * + * Notation: + * C: the pie center + * s: the bisector ray of the current pie slice + * alpha: the angle between the horizontal axis and the bisector ray s + * N: the vertex of the label b.b. which is nearest to C + * F: the vertex of the label b.b. not adjacent to N; F lies on the pie border + * P, Q: the intersection points between the label b.b. and the bisector ray s; + * P is the one at minimum distance respect with C + * e: the edge of the label b.b. where P lies (the nearest edge to C) + * M: the vertex of e that is not N + * G: the vertex of the label b.b. which is adjacent to N and that is not M + * beta: the angle MPF + * theta: the angle CPF + * + * + * | + * | /s + * | / + * | / + * | G _________________________/____________________________ F + * | | /Q ..| + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / d. . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . . | + * | | / . \ beta . | + * | |__________/._\___|_______.____________________________| + * | N /P / . M + * | /___/theta . + * | / . + * | / . r + * | / . + * | / . + * | / . + * | / . + * | / . + * | / . + * | / . + * | / . + * | /\. alpha + * __|/__|_____________________________________________________________ + * |C + * | + * + * + * When alpha = 45k (k integer) s crosses the label b.b. at N exactly. + * In such a case the nearest edge e is defined as the edge having N as the + * start vertex and that is covered in the counterclockwise direction when + * we move from N to the adjacent vertex. + * + * The nearest vertex N is: + * 1. the bottom left vertex when 0 < alpha < 90 + * 2. the bottom right vertex when 90 < alpha < 180 + * 3. the top right vertex when 180 < alpha < 270 + * 4. the top left vertex when 270 < alpha < 360. + * + * The nearest edge e is: + * 1. the left edge when −45 < alpha < 45 + * 2. the bottom edge when 45 < alpha <135 + * 3. the right edge when 135 < alpha < 225 + * 4. the top edge when 225 < alpha < 315. + * + **/ +bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam, PieLabelInfo const & rPieLabelInfo) +{ + SAL_INFO( "chart2.pie.label.bestfit.inside", + "** PieChart::performLabelBestFitInnerPlacement invoked **" ); + + // get pie slice properties + double fStartAngleDeg = NormAngle360(rShapeParam.mfUnitCircleStartAngleDegree); + double fWidthAngleDeg = rShapeParam.mfUnitCircleWidthAngleDegree; + double fHalfWidthAngleDeg = fWidthAngleDeg / 2.0; + double fBisectingRayAngleDeg = NormAngle360(fStartAngleDeg + fHalfWidthAngleDeg); + + // get the middle point of the arc representing the pie slice border + double fLogicZ = rShapeParam.mfLogicZ + 1.0; + awt::Point aMiddleArcPoint = PlottingPositionHelper::transformSceneToScreenPosition( + m_pPosHelper->transformUnitCircleToScene( + fBisectingRayAngleDeg, + rShapeParam.mfUnitCircleOuterRadius, + fLogicZ ), + m_xLogicTarget, m_nDimension ); + + // compute the pie radius + basegfx::B2IVector aPieCenter = rPieLabelInfo.aOrigin; + basegfx::B2IVector aRadiusVector( + aMiddleArcPoint.X - aPieCenter.getX(), + aMiddleArcPoint.Y - aPieCenter.getY() ); + double fSquaredPieRadius = aRadiusVector.scalar(aRadiusVector); + double fPieRadius = sqrt( fSquaredPieRadius ); + + // the bb is moved as much as possible near to the border of the pie, + // anyway a small offset from the border is present (0.025 * pie radius) + const double fPieBorderOffset = 0.025; + fPieRadius = fPieRadius - fPieRadius * fPieBorderOffset; + + SAL_INFO( "chart2.pie.label.bestfit.inside", + " pie sector:" ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " start angle = " << fStartAngleDeg ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " angle width = " << fWidthAngleDeg ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " bisecting ray angle = " << fBisectingRayAngleDeg ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " pie radius = " << fPieRadius ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " pie center = " << rPieLabelInfo.aOrigin ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " middle arc point = (" << aMiddleArcPoint.X << "," + << aMiddleArcPoint.Y << ")" ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " label bounding box:" ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " old anchor point = " << rPieLabelInfo.aFirstPosition ); + + + if( fPieRadius == 0.0 ) + return false; + + // get label b.b. width and height + ::basegfx::B2IRectangle aBb( lcl_getRect( rPieLabelInfo.xLabelGroupShape ) ); + double fLabelWidth = aBb.getWidth(); + double fLabelHeight = aBb.getHeight(); + + // -45 <= fAlphaDeg < 315 + double fAlphaDeg = NormAngle360(fBisectingRayAngleDeg + 45) - 45; + double fAlphaRad = basegfx::deg2rad(fAlphaDeg); + + // compute nearest edge index + // 0 left + // 1 bottom + // 2 right + // 3 top + int nSectorIndex = floor( (fAlphaDeg + 45) / 45.0 ); + int nNearestEdgeIndex = nSectorIndex / 2; + + // compute lengths of the nearest edge and of the orthogonal edges + double fNearestEdgeLength = fLabelWidth; + double fOrthogonalEdgeLength = fLabelHeight; + basegfx::Axis2D eAxis = basegfx::Axis2D::X; + basegfx::Axis2D eOrthogonalAxis = basegfx::Axis2D::Y; + if( nNearestEdgeIndex % 2 == 0 ) // nearest edge is vertical + { + fNearestEdgeLength = fLabelHeight; + fOrthogonalEdgeLength = fLabelWidth; + eAxis = basegfx::Axis2D::Y; + eOrthogonalAxis = basegfx::Axis2D::X; + } + + // compute the distance between N and P + // such a distance is piece wise linear respect with alpha: + // given 45k <= alpha < 45(k+1) we have + // when k is even: d(N,P) = (length(e) / 2) * (1 - (alpha - 45k)/45) + // when k is odd: d(N,P) = (length(e) / 2) * (1 - (45(k+1) - alpha)/45) + int nIndex = nSectorIndex -1; // nIndex = -1...6 + double fIndexMod2 = (nIndex + 8) % 2; // fIndexMod2 must be non negative + double fSgn = 2.0 * (fIndexMod2 - 0.5); // 0 -> -1, 1 -> 1 + double fDistanceNP = (fNearestEdgeLength / 2.0) * (1 + fSgn * ((fAlphaDeg - 45 * (nIndex + fIndexMod2)) / 45.0)); + double fDistancePM = fNearestEdgeLength - fDistanceNP; + + // compute the length of the diagonal vector d, + // that is the distance between P and F + double fDistancePF = std::hypot(fDistancePM, fOrthogonalEdgeLength); + + SAL_INFO( "chart2.pie.label.bestfit.inside", + " width = " << fLabelWidth ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " height = " << fLabelHeight ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " nearest edge index = " << nNearestEdgeIndex ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " alpha = " << fAlphaDeg ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " distance(N,P) = " << fDistanceNP ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " nIndex = " << nIndex ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " fIndexMod2 = " << fIndexMod2 ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " fSgn = " << fSgn ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " distance(P,F) = " << fDistancePF ); + + + // we check that the condition length(d) <= pie radius holds + if (fDistancePF > fPieRadius) + { + return false; + } + + // compute beta: the angle of the diagonal vector d, + // that is, the angle in P respect with the triangle PMF; + // since both arguments are non negative the returned value is in [0, PI/2] + double fBetaRad = atan2( fOrthogonalEdgeLength, fDistancePM ); + + // compute the theta angle, that is the angle in P + // respect with the triangle CFP; + // when the second intersection edge is opposite to the nearest edge, + // theta depends on alpha and beta according to the following relation: + // theta = f(alpha, beta) = s * alpha + 90 * (1 - s * i) + beta + // where i is the nearest edge index and s is the sign of (alpha' - 45), + // with alpha' = (alpha + 45) mod 90; + // when the second intersection edge is adjacent to the nearest edge, + // we have theta = 360 - f(alpha, beta); + // note that in the former case 0 <= f(alpha, beta) <= 180, + // whilst in the latter case 180 <= f(alpha, beta) <= 360; + double fAlphaMod90 = fmod( fAlphaDeg + 45, 90.0 ) - 45; + double fSign = fAlphaMod90 == 0.0 + ? 0.0 + : ( fAlphaMod90 < 0 ) ? -1.0 : 1.0; + double fThetaRad = fSign * fAlphaRad + M_PI_2 * (1 - fSign * nNearestEdgeIndex) + fBetaRad; + if( fThetaRad > M_PI ) + { + fThetaRad = 2 * M_PI - fThetaRad; + } + + // compute the length of the positional vector, + // that is the distance between C and P + double fDistanceCP; + // when the bisector ray intersects the b.b. in F we have theta mod 180 == 0 + if( fmod(fThetaRad, M_PI) == 0.0 ) + { + fDistanceCP = fPieRadius - fDistancePF; + } + else // general case + { + // we can compute d(C,P) by applying some trigonometric formula to + // the triangle CFP : we know length(d) and length(r) = r and we have + // computed the angle in P (theta); so named delta the angle in C and + // gamma the angle in F, by the relation: + // + // r d(P,F) d(C,P) + // --------- = --------- = --------- + // sin theta sin delta sin gamma + // + // we get the wanted distance + double fSinTheta = sin( fThetaRad ); + double fSinDelta = fDistancePF * fSinTheta / fPieRadius; + double fDeltaRad = asin( fSinDelta ); + double fGammaRad = M_PI - (fThetaRad + fDeltaRad); + double fSinGamma = sin( fGammaRad ); + fDistanceCP = fPieRadius * fSinGamma / fSinTheta; + } + + // define the positional vector + basegfx::B2DVector aPositionalVector( cos(fAlphaRad), sin(fAlphaRad) ); + aPositionalVector.setLength(fDistanceCP); + + // we define a direction vector in order to know + // in which quadrant we are working + basegfx::B2DVector aDirection(1.0, 1.0); + if( 90 <= fBisectingRayAngleDeg && fBisectingRayAngleDeg < 270 ) + { + aDirection.setX(-1.0); + } + if( fBisectingRayAngleDeg >= 180 ) + { + aDirection.setY(-1.0); + } + + // compute vertices N, M and G respect with pie center C + basegfx::B2DVector aNearestVertex(aPositionalVector); + aNearestVertex.set(eAxis, aNearestVertex.get(eAxis) - aDirection.get(eAxis) * fDistanceNP); + basegfx::B2DVector aVertexM(aNearestVertex); + aVertexM.set(eAxis, aVertexM.get(eAxis) + aDirection.get(eAxis) * fNearestEdgeLength); + basegfx::B2DVector aVertexG(aNearestVertex); + aVertexG.set(eOrthogonalAxis, aVertexG.get(eOrthogonalAxis) + aDirection.get(eOrthogonalAxis) * fOrthogonalEdgeLength); + + SAL_INFO( "chart2.pie.label.bestfit.inside", + " beta = " << basegfx::rad2deg(fBetaRad) ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " theta = " << basegfx::rad2deg(fThetaRad) ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " fAlphaMod90 = " << fAlphaMod90 ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " fSign = " << fSign ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " distance(C,P) = " << fDistanceCP ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " direction vector = " << aDirection ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " N = " << aNearestVertex ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " M = " << aVertexM ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " G = " << aVertexG ); + + // in order to be able to place the label inside the pie slice we need + // to check that each angle between s and the ray starting from C and + // passing through a b.b. vertex is less than half width of the pie slice; + // when the nearest edge e crosses a Cartesian axis it is sufficient + // to test only the vertices belonging to e, else we need to test + // the 2 vertices that aren't either N or F. Note that if a b.b. edge + // crosses a Cartesian axis then it is the nearest edge to C + + // check the angle between CP and CM + double fAngleRad = aPositionalVector.angle(aVertexM); + double fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad)); + if( fAngleDeg > 180 ) // in case the wrong angle has been computed + fAngleDeg = 360 - fAngleDeg; + SAL_INFO( "chart2.pie.label.bestfit.inside", + " angle between CP and CM: " << fAngleDeg ); + if( fAngleDeg > fHalfWidthAngleDeg ) + { + return false; + } + + if( ( aNearestVertex.get(eAxis) >= 0 && aVertexM.get(eAxis) <= 0 ) + || ( aNearestVertex.get(eAxis) <= 0 && aVertexM.get(eAxis) >= 0 ) ) + { + // check the angle between CP and CN + fAngleRad = aPositionalVector.angle(aNearestVertex); + fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad)); + if( fAngleDeg > 180 ) // in case the wrong angle has been computed + fAngleDeg = 360 - fAngleDeg; + SAL_INFO( "chart2.pie.label.bestfit.inside", + " angle between CP and CN: " << fAngleDeg ); + if( fAngleDeg > fHalfWidthAngleDeg ) + { + return false; + } + } + else + { + // check the angle between CP and CG + fAngleRad = aPositionalVector.angle(aVertexG); + fAngleDeg = NormAngle360(basegfx::rad2deg(fAngleRad)); + if( fAngleDeg > 180 ) // in case the wrong angle has been computed + fAngleDeg = 360 - fAngleDeg; + SAL_INFO( "chart2.pie.label.bestfit.inside", + " angle between CP and CG: " << fAngleDeg ); + if( fAngleDeg > fHalfWidthAngleDeg ) + { + return false; + } + } + + // compute the b.b. center respect with the pie center + basegfx::B2DVector aBBCenter(aNearestVertex); + aBBCenter.set(eAxis, aBBCenter.get(eAxis) + aDirection.get(eAxis) * fNearestEdgeLength / 2); + aBBCenter.set(eOrthogonalAxis, aBBCenter.get(eOrthogonalAxis) + aDirection.get(eOrthogonalAxis) * fOrthogonalEdgeLength / 2); + + // compute the b.b. anchor point + basegfx::B2IVector aNewAnchorPoint = aPieCenter; + aNewAnchorPoint.setX(aNewAnchorPoint.getX() + floor(aBBCenter.getX())); + aNewAnchorPoint.setY(aNewAnchorPoint.getY() - floor(aBBCenter.getY())); // the Y axis on the screen points downward + + // compute the translation vector for moving the label from the current + // screen position to the new one + basegfx::B2IVector aTranslationVector = aNewAnchorPoint - rPieLabelInfo.aFirstPosition; + + // compute the new screen position and move the label + // XShape::getPosition returns the top left vertex of the b.b. of the shape + awt::Point aOldPos( rPieLabelInfo.xLabelGroupShape->getPosition() ); + awt::Point aNewPos( aOldPos.X + aTranslationVector.getX(), + aOldPos.Y + aTranslationVector.getY() ); + rPieLabelInfo.xLabelGroupShape->setPosition(aNewPos); + + SAL_INFO( "chart2.pie.label.bestfit.inside", + " center = " << aBBCenter ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " new anchor point = " << aNewAnchorPoint ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " translation vector = " << aTranslationVector ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " old position = (" << aOldPos.X << "," << aOldPos.Y << ")" ); + SAL_INFO( "chart2.pie.label.bestfit.inside", + " new position = (" << aNewPos.X << "," << aNewPos.Y << ")" ); + + return true; +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |