diff options
Diffstat (limited to 'chart2/source/view/charttypes/PieChart.cxx')
-rw-r--r-- | chart2/source/view/charttypes/PieChart.cxx | 1179 |
1 files changed, 864 insertions, 315 deletions
diff --git a/chart2/source/view/charttypes/PieChart.cxx b/chart2/source/view/charttypes/PieChart.cxx index 57dbab089e00..ff8cf62f133a 100644 --- a/chart2/source/view/charttypes/PieChart.cxx +++ b/chart2/source/view/charttypes/PieChart.cxx @@ -20,23 +20,21 @@ #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 <DataSeries.hxx> +#include <DataSeriesProperties.hxx> #include <com/sun/star/chart/DataLabelPlacement.hpp> -#include <com/sun/star/chart2/XChartType.hpp> #include <com/sun/star/chart2/XColorScheme.hpp> -#include <com/sun/star/container/XChild.hpp> -#include <com/sun/star/drawing/XShape.hpp> #include <com/sun/star/drawing/XShapes.hpp> -#include <com/sun/star/beans/XPropertySet.hpp> #include <sal/log.hxx> #include <osl/diagnose.h> -#include <tools/diagnose_ex.h> +#include <comphelper/diagnose_ex.hxx> #include <tools/helpers.hxx> #include <limits> @@ -44,6 +42,7 @@ using namespace ::com::sun::star; using namespace ::com::sun::star::chart2; +using namespace ::chart::DataSeriesProperties; namespace chart { @@ -104,7 +103,7 @@ struct PieChart::ShapeParam namespace { -::basegfx::B2IRectangle lcl_getRect(const uno::Reference<drawing::XShape>& xShape) +::basegfx::B2IRectangle lcl_getRect(const rtl::Reference<SvxShape>& xShape) { ::basegfx::B2IRectangle aRect; if (xShape.is()) @@ -125,18 +124,6 @@ bool lcl_isInsidePage(const awt::Point& rPos, const awt::Size& rSize, const awt: } //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) { @@ -191,38 +178,54 @@ bool PiePositionHelper::getInnerAndOuterRadius( double fCategoryX return true; } -PieChart::PieChart( const uno::Reference<XChartType>& xChartTypeModel + +bool PiePositionHelper::clockwiseWedges() const +{ + const ExplicitScaleData& rAngleScale = m_bSwapXAndY ? m_aScales[1] : m_aScales[0]; + return rAngleScale.Orientation == AxisOrientation_REVERSE; +} + + +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_aPosHelper( (m_nDimension==3) ? 0.0 : 90.0 ) , m_bUseRings(false) , m_bSizeExcludesLabelsAndExplodedSegments(bExcludingPositioning) + , m_eSubType(PieChartSubType_NONE) , 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; + PlotterBase::m_pPosHelper = &m_aPosHelper; + VSeriesPlotter::m_pMainPosHelper = &m_aPosHelper; + m_aPosHelper.m_fRadiusOffset = 0.0; + m_aPosHelper.m_fRingDistance = 0.0; - uno::Reference< beans::XPropertySet > xChartTypeProps( xChartTypeModel, uno::UNO_QUERY ); - if( !xChartTypeProps.is() ) + if( !xChartTypeModel.is() ) return; try { - xChartTypeProps->getPropertyValue( "UseRings") >>= m_bUseRings; + xChartTypeModel->getFastPropertyValue(PROP_PIECHARTTYPE_USE_RINGS) >>= m_bUseRings; // "UseRings" if( m_bUseRings ) { - m_pPosHelper->m_fRadiusOffset = 1.0; + m_aPosHelper.m_fRadiusOffset = 1.0; if( nDimensionCount==3 ) - m_pPosHelper->m_fRingDistance = 0.1; + m_aPosHelper.m_fRingDistance = 0.1; } } catch( const uno::Exception& ) { TOOLS_WARN_EXCEPTION("chart2", "" ); } + try + { + xChartTypeModel->getFastPropertyValue(PROP_PIECHARTTYPE_SUBTYPE) >>= m_eSubType; // "SubType" + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } } PieChart::~PieChart() @@ -232,7 +235,7 @@ 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 ); + m_aPosHelper.setScales( std::move(rScales), true ); } drawing::Direction3D PieChart::getPreferredDiagramAspectRatio() const @@ -247,46 +250,143 @@ bool PieChart::shouldSnapRectToUsedArea() return true; } -uno::Reference< drawing::XShape > PieChart::createDataPoint( - const uno::Reference<drawing::XShapes>& xTarget, +rtl::Reference<SvxShape> PieChart::createDataPoint( + const SubPieType e_subType, + const rtl::Reference<SvxShapeGroupAnyD>& xTarget, const uno::Reference<beans::XPropertySet>& xObjectProperties, - tPropertyNameValueMap const * pOverwritePropertiesMap, - const ShapeParam& rParam ) + const ShapeParam& rParam, + const sal_Int32 nPointCount, + const bool bConcentricExplosion) { //transform position: drawing::Direction3D aOffset; - if (rParam.mfExplodePercentage != 0.0) - { - double fAngle = rParam.mfUnitCircleStartAngleDegree + rParam.mfUnitCircleWidthAngleDegree/2.0; - double fRadius = (rParam.mfUnitCircleOuterRadius-rParam.mfUnitCircleInnerRadius)*rParam.mfExplodePercentage; - drawing::Position3D aOrigin = m_pPosHelper->transformUnitCircleToScene(0, 0, rParam.mfLogicZ); - drawing::Position3D aNewOrigin = m_pPosHelper->transformUnitCircleToScene(fAngle, fRadius, rParam.mfLogicZ); - aOffset = aNewOrigin - aOrigin; + 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_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ); + drawing::Position3D aNewOrigin = m_aPosHelper.transformUnitCircleToScene(fAngle, fRadius, rParam.mfLogicZ); + aOffset = aNewOrigin - aOrigin; + } + } else { + drawing::Position3D aOrigin, aNewOrigin; + switch (e_subType) { + case SubPieType::LEFT: + // Draw the main pie for bar-of-pie/pie-of-pie smaller and to the left + aOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ); + aNewOrigin = m_aPosHelper.transformUnitCircleToScene(180, 0.75, rParam.mfLogicZ); + aOffset = aNewOrigin - aOrigin; + fExplodedOuterRadius *= 2.0/3; + break; + case SubPieType::RIGHT: + // Draw the sub-pie for pie-of-pie much smaller and to the right + aOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0, rParam.mfLogicZ); + aNewOrigin = m_aPosHelper.transformUnitCircleToScene(0, 0.75, rParam.mfLogicZ); + aOffset = aNewOrigin - aOrigin; + fExplodedOuterRadius *= 1.0/3; + break; + case SubPieType::NONE: + default: + // no change + break; + } } + //create point - uno::Reference< drawing::XShape > xShape; + rtl::Reference<SvxShape> xShape; if(m_nDimension==3) { - xShape = m_pShapeFactory->createPieSegment( xTarget - , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree - , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius - , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() ) + xShape = ShapeFactory::createPieSegment( xTarget + , fStartAngle, fWidthAngle + , fExplodedInnerRadius, fExplodedOuterRadius + , aOffset, B3DHomMatrixToHomogenMatrix( m_aPosHelper.getUnitCartesianToScene() ) , rParam.mfDepth ); } else { - xShape = m_pShapeFactory->createPieSegment2D( xTarget - , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree - , rParam.mfUnitCircleInnerRadius, rParam.mfUnitCircleOuterRadius - , aOffset, B3DHomMatrixToHomogenMatrix( m_pPosHelper->getUnitCartesianToScene() ) ); + xShape = ShapeFactory::createPieSegment2D( xTarget + , fStartAngle, fWidthAngle + , fExplodedInnerRadius, fExplodedOuterRadius + , aOffset, B3DHomMatrixToHomogenMatrix( m_aPosHelper.getUnitCartesianToScene() ) ); } - setMappedProperties( xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties(), pOverwritePropertiesMap ); + PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + return xShape; +} + +rtl::Reference<SvxShape> PieChart::createBarDataPoint( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget, + const uno::Reference<beans::XPropertySet>& xObjectProperties, + const ShapeParam& rParam, + double fBarSegBottom, double fBarSegTop) +{ + drawing::Position3D aP0, aP1; + + // Draw the bar for bar-of-pie small and to the right. Width and + // position are hard-coded for now. + +#if 0 + aP0 = cartesianPosHelper.transformLogicToScene(0.75, fBarSegBottom, + rParam.mfLogicZ, false); + aP1 = cartesianPosHelper.transformLogicToScene(1.25, fBarSegTop, + rParam.mfLogicZ, false); +#else + double x0 = m_aPosHelper.transformUnitCircleToScene(0, 0.75, 0).PositionX; + double x1 = m_aPosHelper.transformUnitCircleToScene(0, 1.25, 0).PositionX; + double y0 = m_aPosHelper.transformUnitCircleToScene( + 90, fBarSegBottom, 0).PositionY; + double y1 = m_aPosHelper.transformUnitCircleToScene( + 90, fBarSegTop, 0).PositionY; + + aP0 = drawing::Position3D(x0, y0, rParam.mfLogicZ); + aP1 = drawing::Position3D(x1, y1, rParam.mfLogicZ); +#endif + + const css::awt::Point aPos(aP0.PositionX, aP1.PositionY); + const css::awt::Size aSz(fabs(aP0.PositionX - aP1.PositionX), + fabs(aP0.PositionY - aP1.PositionY)); + + const tNameSequence emptyNameSeq; + const tAnySequence emptyValSeq; + //create point + rtl::Reference<SvxShape> xShape = ShapeFactory::createRectangle( + xTarget, + aSz, aPos, + emptyNameSeq, emptyValSeq); + + PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); return xShape; } void PieChart::createTextLabelShape( - const uno::Reference<drawing::XShapes>& xTextTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, VDataSeries& rSeries, sal_Int32 nPointIndex, ShapeParam& rParam ) { if (!rSeries.getDataPointLabelIfLabel(nPointIndex)) @@ -307,7 +407,12 @@ void PieChart::createTextLabelShape( ///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()); + nPointIndex, m_xChartTypeModel, m_aPosHelper.isSwapXAndY()); + + // has an X/Y offset (relative to the OUTSIDE label default position) been provided? + const bool bHasCustomLabelPlacement = nLabelPlacement == css::chart::DataLabelPlacement::CUSTOM; + if (bHasCustomLabelPlacement) + nLabelPlacement = css::chart::DataLabelPlacement::OUTSIDE; ///when the placement is of `AVOID_OVERLAP` type a later rearrangement of ///the label position is allowed; the `createTextLabelShape` treats the @@ -315,8 +420,7 @@ void PieChart::createTextLabelShape( 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; + bool bMovementAllowed = nLabelPlacement == css::chart::DataLabelPlacement::AVOID_OVERLAP; if( bMovementAllowed ) nLabelPlacement = css::chart::DataLabelPlacement::CENTER; @@ -338,7 +442,7 @@ void PieChart::createTextLabelShape( ///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,m_pShapeFactory); + PolarLabelPositionHelper aPolarPosHelper(&m_aPosHelper,m_nDimension,m_xLogicTarget); awt::Point aScreenPosition2D( aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues(eAlignment, nLabelPlacement , rParam.mfUnitCircleStartAngleDegree, rParam.mfUnitCircleWidthAngleDegree @@ -347,7 +451,7 @@ void PieChart::createTextLabelShape( ///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 ) ) ); + awt::Point aOrigin( aPolarPosHelper.transformSceneToScreenPosition( m_aPosHelper.transformUnitCircleToScene( 0.0, 0.0, rParam.mfLogicZ+1.0 ) ) ); aPieLabelInfo.aOrigin = basegfx::B2IVector( aOrigin.X, aOrigin.Y ); ///add a scaling independent Offset if requested @@ -361,39 +465,81 @@ void PieChart::createTextLabelShape( // compute outer pie radius awt::Point aOuterCirclePoint = PlottingPositionHelper::transformSceneToScreenPosition( - m_pPosHelper->transformUnitCircleToScene( + m_aPosHelper.transformUnitCircleToScene( 0, rParam.mfUnitCircleOuterRadius, 0 ), - m_xLogicTarget, m_pShapeFactory, m_nDimension ); + 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; + const double fHalfWidthAngleDegree = rParam.mfUnitCircleWidthAngleDegree / 2.0; + // fAngleDegree: the angle through the center of the slice / the bisecting ray + const double fAngleDegree + = NormAngle360(rParam.mfUnitCircleStartAngleDegree + fHalfWidthAngleDegree); + // aOuterPosition: slice midpoint on the circumference, + // which is where an outside/custom label would be connected awt::Point aOuterPosition = PlottingPositionHelper::transformSceneToScreenPosition( - m_pPosHelper->transformUnitCircleToScene(fAngleDegree, rParam.mfUnitCircleOuterRadius, 0), - m_xLogicTarget, m_pShapeFactory, m_nDimension); + m_aPosHelper.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 + /* There are basically three places where a label could be placed in a pie chart + * 1.) outside the slice + * -typically used for long labels or charts with many, thin slices + * 2.) inside the slice (center or edge) + * -typically used for charts with 5 or less slices + * 3.) in a custom location + * -typically set (by auto-positioning I presume) when labels overlap + * + * Selecting a good width for the text is critical to achieving good-looking labels. + * Our bestFit algorithm completely depends on a good starting guess. + * Lots of room for improvement here... + * Warning: complication due to 3D ovals (so can't use normal circle functions), + * donuts(m_bUseRings), auto re-scaling of the pie chart, etc. + * + * Based on observation, Microsoft uses 1/5 of the chart space as its text limit, + * although it will reduce the width (as long as it is not a custom position) + * if doing so means that the now-taller-text will fit inside the slice, + * so best if we do the same for our charts. + */ + + // set the maximum text width to be used when text wrapping is enabled (default text wrap is on) + /* A reasonable start for bestFitting a 90deg slice oriented on an Axis is 80% of the radius */ double fTextMaximumFrameWidth = 0.8 * fPieRadius; - if (nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE - && m_aAvailableOuterRect.getWidth()) + const double fCompatMaxTextLen = m_aAvailableOuterRect.getWidth() / 5.0; + 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); + if (bHasCustomLabelPlacement) + { + // if a custom width has been provided, then use that of course, + // otherwise use the interoperability-compliant 1/5 of the chart space as max width + const awt::Size aCustomSize = rSeries.getLabelCustomSize(nPointIndex); + if (aCustomSize.Width > 0) + fTextMaximumFrameWidth = aCustomSize.Width; + else + fTextMaximumFrameWidth = fCompatMaxTextLen; + } + else if (nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE) + { + // use up to 80% of the available space from the slice edge to the edge of the chart + const sal_Int32 nOuterX = aPieLabelInfo.aOuterPosition.getX(); + if (fAngleDegree < 90 || fAngleDegree > 270) // label is placed on the right side + fTextMaximumFrameWidth = 0.8 * abs(m_aAvailableOuterRect.getWidth() - nOuterX); + else // label is placed on the left side + fTextMaximumFrameWidth = 0.8 * nOuterX; + + // limited of course to the 1/5 maximum allowed for compatibility + fTextMaximumFrameWidth = std::min(fTextMaximumFrameWidth, fCompatMaxTextLen); + } } + /* TODO: better guesses for INSIDE: does the slice better handle wide text or tall/wrapped text? + * * wide: center near X-axis, shorter text content, slice > 90degree wide + * * tall: center near Y-axis, longer text content, many categories shown + */ sal_Int32 nTextMaximumFrameWidth = ceil(fTextMaximumFrameWidth); ///the text shape for the label is created @@ -403,13 +549,13 @@ void PieChart::createTextLabelShape( ///a new `PieLabelInfo` instance is initialized with all the info related to ///the current label in order to simplify later label position rearrangement; - uno::Reference< container::XChild > xChild( aPieLabelInfo.xTextShape, uno::UNO_QUERY ); + 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.set( xChild->getParent(), uno::UNO_QUERY ); + aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get()); if (bMovementAllowed && !m_bUseRings) { @@ -417,21 +563,47 @@ void PieChart::createTextLabelShape( * 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)) + + /* Note: bestFit surprisingly does not adjust the width of the label, + * so having an optimal width already set when createDataLabel ran earlier + * is crucial (and currently lacking)! + * TODO: * change bestFit to treat the width as a max width, and reduce if beneficial + */ + if (!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); + /* This tried to bestFit, but it didn't fit. So how best to handle this? + * + * Two possible cases relating to compatibility + * 1.) It did fit for Microsoft, but our bestFit wasn't able to do the same + * * In that case, the best response is to be as small as possible + * (the distance from the chart edge to where the label attaches to the slice) + * to avoid scaling the diagram with too long outside labels, + * and to encourage fixing the bestFit algorithm. + * 2.) It didn't fit for Microsoft either (possible, but less likely situation) + * * In that case, the compatible max length would be best + * * can expect the chart space has been properly sized to handle the max length + * + * In the native LO case, it is also best to be as small as possible, + * so that the user creating the diagram is annoyed and makes the chart area larger. + * + * Therefore, handle this by making the label as small as possible. + * + * Complication (tdf122765.pptx): it is possible for the aOuterPosition + * to be outside of the available outer rectangle (somehow), + * so in that bizarre case just try the positive value of the result... + */ + const sal_Int32 nOuterX = aPieLabelInfo.aOuterPosition.getX(); + if (fAngleDegree < 90 || fAngleDegree > 270) // label is placed on the right side + fTextMaximumFrameWidth = 0.8 * abs(m_aAvailableOuterRect.getWidth() - nOuterX); + else // label is placed on the left side + fTextMaximumFrameWidth = 0.8 * nOuterX; + + nTextMaximumFrameWidth = ceil(std::min(fTextMaximumFrameWidth, fCompatMaxTextLen)); } + // find the position to connect an Outside label to nScreenValueOffsetInRadiusDirection = (m_nDimension != 3) ? 150 : 0; aScreenPosition2D = aPolarPosHelper.getLabelScreenPositionAndAlignmentForUnitCircleValues( @@ -453,21 +625,21 @@ void PieChart::createTextLabelShape( } uno::Reference<drawing::XShapes> xShapes(xChild->getParent(), uno::UNO_QUERY); + /* question: why remove and rebuild? Can't the existing one just be changed? */ xShapes->remove(aPieLabelInfo.xTextShape); aPieLabelInfo.xTextShape = createDataLabel(xTextTarget, rSeries, nPointIndex, nVal, rParam.mfLogicYSum, aScreenPosition2D, eAlignment, 0, nTextMaximumFrameWidth); - xChild.clear(); - xChild.set(uno::Reference<container::XChild>(aPieLabelInfo.xTextShape, uno::UNO_QUERY)); + xChild = aPieLabelInfo.xTextShape; if (!xChild.is()) return; - aPieLabelInfo.xLabelGroupShape.set(xChild->getParent(), uno::UNO_QUERY); + aPieLabelInfo.xLabelGroupShape = dynamic_cast<SvxShapeGroupAnyD*>(xChild->getParent().get()); } } - bool bShowLeaderLine = rSeries.getPropertiesOfSeries() - ->getPropertyValue("ShowCustomLeaderLines") + bool bShowLeaderLine = rSeries.getModel() + ->getFastPropertyValue(PROP_DATASERIES_SHOW_CUSTOM_LEADERLINES) // "ShowCustomLeaderLines" .get<sal_Bool>(); if (m_bPieLabelsAllowToMove) { @@ -494,44 +666,35 @@ void PieChart::createTextLabelShape( { 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 + const sal_Int32 nX2 = std::clamp(nX1, aRect.getMinX(), aRect.getMaxX()); + const sal_Int32 nY2 = std::clamp(nY1, aRect.getMinY(), aRect.getMaxY()); + + const sal_Int32 nLabelSquaredDistanceFromOrigin = (nX2 - aOrigin.X) * (nX2 - aOrigin.X) + (nY2 - aOrigin.Y) * (nY2 - aOrigin.Y); + // can't use fSquaredPieRadius for 3D charts, since no longer a true circle + const sal_Int32 nPieEdgeSquaredDistanceFromOrigin + = (nX1 - aOrigin.X) * (nX1 - aOrigin.X) + (nY1 - aOrigin.Y) * (nY1 - aOrigin.Y); // tdf#138018 Don't show leader line when custom positioned data label is inside pie chart - if (nSquaredDistanceFromOrigin > fSquaredPieRadius) + if (nLabelSquaredDistanceFromOrigin > nPieEdgeSquaredDistanceFromOrigin) { //when the line is very short compared to the page size don't create one ::basegfx::B2DVector aLength(nX1 - nX2, nY1 - nY2); - double fPageDiagonaleLength = sqrt(double(nPageWidth) * double(nPageWidth) - + double(nPageHeight) * double(nPageHeight)); + double fPageDiagonaleLength = std::hypot(nPageWidth, nPageHeight); if ((aLength.getLength() / fPageDiagonaleLength) >= 0.01) { drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } }; - uno::Reference<beans::XPropertySet> xProp(aPieLabelInfo.xTextShape, - uno::UNO_QUERY); VLineProperties aVLineProperties; - if (xProp.is()) + if (aPieLabelInfo.xTextShape.is()) { sal_Int32 nColor = 0; - xProp->getPropertyValue("CharColor") >>= nColor; + aPieLabelInfo.xTextShape->SvxShape::getPropertyValue("CharColor") >>= nColor; //automatic font color does not work for lines -> fallback to black if (nColor != -1) aVLineProperties.Color <<= nColor; } - m_pShapeFactory->createLine2D(xTextTarget, aPoints, &aVLineProperties); + ShapeFactory::createLine2D(xTextTarget, aPoints, &aVLineProperties); } } } @@ -572,19 +735,20 @@ double PieChart::getMaxOffset() return m_fMaxOffset; VDataSeries* pSeries = rSeriesList.front().get(); - uno::Reference< beans::XPropertySet > xSeriesProp( pSeries->getPropertiesOfSeries() ); - if( !xSeriesProp.is() ) + rtl::Reference< DataSeries > xSeries( pSeries->getModel() ); + if( !xSeries.is() ) return m_fMaxOffset; double fExplodePercentage=0.0; - xSeriesProp->getPropertyValue( "Offset") >>= fExplodePercentage; + xSeries->getPropertyValue( "Offset") >>= fExplodePercentage; if(fExplodePercentage>m_fMaxOffset) m_fMaxOffset=fExplodePercentage; if(!m_bSizeExcludesLabelsAndExplodedSegments) { uno::Sequence< sal_Int32 > aAttributedDataPointIndexList; - if( xSeriesProp->getPropertyValue( "AttributedDataPoints" ) >>= aAttributedDataPointIndexList ) + // "AttributedDataPoints" + if( xSeries->getFastPropertyValue( PROP_DATASERIES_ATTRIBUTED_DATA_POINTS ) >>= aAttributedDataPointIndexList ) { for(sal_Int32 nN=aAttributedDataPointIndexList.getLength();nN--;) { @@ -664,18 +828,16 @@ void PieChart::createShapes() ///( a member of a VDiagram object); this initialization occurs in ///`ChartView::impl_createDiagramAndContent`. - OSL_ENSURE(m_pShapeFactory && m_xLogicTarget.is() && m_xFinalTarget.is(), "PieChart is not properly initialized."); - if (!m_pShapeFactory || !m_xLogicTarget.is() || !m_xFinalTarget.is()) + 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) - uno::Reference< drawing::XShapes > xSeriesTarget( - createGroupShape( m_xLogicTarget )); - uno::Reference< drawing::XShapes > xTextTarget( - m_pShapeFactory->createGroup2D( m_xFinalTarget )); + 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 @@ -697,22 +859,14 @@ void PieChart::createShapes() ///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; - uno::Reference< beans::XPropertySet > xPropertySet( m_xChartTypeModel, uno::UNO_QUERY ); - if ( (m_nDimension==3) && xPropertySet.is()) + if ( (m_nDimension==3) && m_xChartTypeModel.is()) { try { - uno::Any aAny = xPropertySet->getPropertyValue( "3DRelativeHeight" ); + uno::Any aAny = m_xChartTypeModel->getFastPropertyValue( PROP_PIECHARTTYPE_3DRELATIVEHEIGHT ); // "3DRelativeHeight" aAny >>= n3DRelativeHeight; } catch (const uno::Exception&) { } @@ -723,8 +877,6 @@ void PieChart::createShapes() ///(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; @@ -732,17 +884,17 @@ void PieChart::createShapes() 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(); + m_aPosHelper.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(); + ShapeParam aParam; + for( nPointIndex = 0; nPointIndex < nPointCount; nPointIndex++ ) { double fY = pSeries->getYValue( nPointIndex ); @@ -755,136 +907,457 @@ void PieChart::createShapes() aParam.mfLogicYSum += fabs(fY); } - if (aParam.mfLogicYSum == 0.0) + 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++ ) + PieDataSrcBase *pDataSrc = nullptr; + PieDataSrc normalPieSrc; + OfPieDataSrc ofPieSrc; + + // Default to regular pie if too few points for of-pie + ::css::chart2::PieChartSubType eSubType = + nPointCount >= OfPieDataSrc::minPoints ? + m_eSubType : + PieChartSubType_NONE; + + switch (eSubType) { + case PieChartSubType_NONE: + pDataSrc = &normalPieSrc; + createOneRing(SubPieType::NONE, fSlotX, aParam, xSeriesTarget, + xTextTarget, pSeries, pDataSrc, n3DRelativeHeight); + break; + case PieChartSubType_BAR: { - 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; + pDataSrc = &ofPieSrc; + createOneRing(SubPieType::LEFT, 0, aParam, xSeriesTarget, + xTextTarget, pSeries, pDataSrc, n3DRelativeHeight); + createOneBar(SubPieType::RIGHT, aParam, xSeriesTarget, + xTextTarget, pSeries, pDataSrc, n3DRelativeHeight); + + // + // Draw connecting lines + // + double xl0, xl1, yl0, yl1, x0, y0, x1, y1, y2, y3; + + // Get coordinates of "corners" of left composite wedge + sal_Int32 nEnd = pDataSrc->getNPoints(pSeries, SubPieType::LEFT); + double compFrac = pDataSrc->getData(pSeries, nEnd - 1, + SubPieType::LEFT) / aParam.mfLogicYSum; + if (compFrac < 0.5) { + xl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale * + cos(compFrac * M_PI) + m_fLeftShift; + yl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale * + sin(compFrac * M_PI); + } else { + xl0 = m_fLeftShift; + yl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale; + } - aParam.mfDepth = getTransformedDepth() * (n3DRelativeHeight / 100.0); + // Coordinates of bar top left corner + xl1 = m_fBarLeft; + yl1 = m_fFullBarHeight / 2; - uno::Reference< drawing::XShapes > 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; + x0 = m_aPosHelper.transformUnitCircleToScene(0, xl0, 0).PositionX; + y0 = m_aPosHelper.transformUnitCircleToScene(90, yl0, 0).PositionY; + x1 = m_aPosHelper.transformUnitCircleToScene(0, xl1, 0).PositionX; + y1 = m_aPosHelper.transformUnitCircleToScene(90, yl1, 0).PositionY; + y2 = m_aPosHelper.transformUnitCircleToScene(90, -yl0, 0).PositionY; + y3 = m_aPosHelper.transformUnitCircleToScene(90, -yl1, 0).PositionY; + + std::vector<std::vector<css::drawing::Position3D>> linePts; + linePts.resize(2); + linePts[0].push_back(css::drawing::Position3D(x0, y0, aParam.mfLogicZ)); + linePts[0].push_back(css::drawing::Position3D(x1, y1, aParam.mfLogicZ)); + linePts[1].push_back(css::drawing::Position3D(x0, y2, aParam.mfLogicZ)); + linePts[1].push_back(css::drawing::Position3D(x1, y3, aParam.mfLogicZ)); + + VLineProperties aVLineProperties; // default black + + //create line + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = + getSeriesGroupShape(pSeries, xSeriesTarget); + rtl::Reference<SvxShape> xShape = ShapeFactory::createLine2D( + xSeriesGroupShape_Shapes, linePts, &aVLineProperties); + + // need to set properties? + //PropertyMapper::setMappedProperties( *xShape, xObjectProperties, + // PropertyMapper::getPropertyNameMapForLineSeriesProperties() ); + + break; + } + case PieChartSubType_PIE: + { + pDataSrc = &ofPieSrc; + createOneRing(SubPieType::LEFT, 0, aParam, xSeriesTarget, + xTextTarget, pSeries, pDataSrc, n3DRelativeHeight); + createOneRing(SubPieType::RIGHT, 0, aParam, xSeriesTarget, + xTextTarget, pSeries, pDataSrc, n3DRelativeHeight); + + // + // Draw connecting lines + // + double xl0, xl1, yl0, yl1, x0, y0, x1, y1, y2, y3; + + // Get coordinates of "corners" of left composite wedge + sal_Int32 nEnd = pDataSrc->getNPoints(pSeries, SubPieType::LEFT); + double compFrac = pDataSrc->getData(pSeries, nEnd - 1, + SubPieType::LEFT) / aParam.mfLogicYSum; + // The following isn't quite right. The tangent points on the left + // pie are only at pi/2 and -pi/2 for composite wedges over 1/2 the + // total if left and right pies are the same diameter. And the + // threshold of 1/2 isn't quite right either. So there + // really should be a more sophisticated approach here. TODO + if (compFrac < 0.5) { + // Translated, per below + xl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale * + cos(compFrac * M_PI) + m_fLeftShift - m_fRightShift; + yl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale * + sin(compFrac * M_PI); + } else { + // Translated, per below + xl0 = m_fLeftShift - m_fRightShift; + yl0 = aParam.mfUnitCircleOuterRadius * m_fLeftScale; + } + + // Compute tangent point on the right-hand circle of the line + // through (xl0, yl0). If we translate things so the right-hand + // circle is centered on the origin, then this point (x,y) + // satisfies these two equations, where r is the radius of the + // right-hand circle: + // (1) x^2 + y^2 = r^2 + // (2) (y - yl0) / (x - xl0) = -x / y + const double r = aParam.mfUnitCircleOuterRadius * m_fRightScale; + + xl1 = (r*r * xl0 + yl0 * r * sqrt(xl0*xl0 + yl0*yl0 - r*r)) / + (xl0*xl0 + yl0*yl0); + yl1 = sqrt(r*r - xl1*xl1); + + // Now translate back to the coordinates we use + xl0 += m_fRightShift; + xl1 += m_fRightShift; + + x0 = m_aPosHelper.transformUnitCircleToScene(0, xl0, 0).PositionX; + y0 = m_aPosHelper.transformUnitCircleToScene(90, yl0, 0).PositionY; + x1 = m_aPosHelper.transformUnitCircleToScene(0, xl1, 0).PositionX; + y1 = m_aPosHelper.transformUnitCircleToScene(90, yl1, 0).PositionY; + y2 = m_aPosHelper.transformUnitCircleToScene(90, -yl0, 0).PositionY; + y3 = m_aPosHelper.transformUnitCircleToScene(90, -yl1, 0).PositionY; + + std::vector<std::vector<css::drawing::Position3D>> linePts; + linePts.resize(2); + linePts[0].push_back(css::drawing::Position3D(x0, y0, aParam.mfLogicZ)); + linePts[0].push_back(css::drawing::Position3D(x1, y1, aParam.mfLogicZ)); + linePts[1].push_back(css::drawing::Position3D(x0, y2, aParam.mfLogicZ)); + linePts[1].push_back(css::drawing::Position3D(x1, y3, aParam.mfLogicZ)); + + VLineProperties aVLineProperties; // default black + + //create line + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = + getSeriesGroupShape(pSeries, xSeriesTarget); + rtl::Reference<SvxShape> xShape = ShapeFactory::createLine2D( + xSeriesGroupShape_Shapes, linePts, &aVLineProperties); + + break; + } + default: + assert(false); // this shouldn't happen + } + }//next x slot +} + +static sal_Int32 propIndex( + sal_Int32 nPointIndex, + enum SubPieType eType, + const PieDataSrcBase *pDataSrc, + VDataSeries* pSeries) +{ + + switch (eType) { + case SubPieType::LEFT: + if (nPointIndex == pDataSrc->getNPoints(pSeries, + SubPieType::LEFT) - 1) { + return pSeries->getTotalPointCount(); + } else { + return nPointIndex; + } + break; + case SubPieType::RIGHT: + return pDataSrc->getNPoints(pSeries, SubPieType::LEFT) + + nPointIndex - 1; + break; + case SubPieType::NONE: + return nPointIndex; + break; + default: // shouldn't happen + assert(false); + return 0; // suppress compile warning + } +} + + +void PieChart::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) +{ + bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor"); - uno::Reference< beans::XPropertySet > xPointProperties = pSeries->getPropertiesOfPoint( nPointIndex ); + sal_Int32 nRingPtCnt = pDataSrc->getNPoints(pSeries, eType); + + // Find sum of entries for this ring or sub-pie + double ringSum = 0; + for (sal_Int32 nPointIndex = 0; nPointIndex < nRingPtCnt; nPointIndex++ ) { + double fY = pDataSrc->getData(pSeries, nPointIndex, eType); + if (!std::isnan(fY) ) ringSum += fY; + } + + // determine the starting angle around the ring + auto sAngle = [&]() + { + if (eType == SubPieType::LEFT) { + // Left of-pie has the "composite" wedge (the one expanded in the right + // subgraph) facing to the right in the chart, to allow the expansion + // lines to meet it + const double compositeVal = pDataSrc->getData(pSeries, nRingPtCnt - 1, eType); + const double degAng = compositeVal * 360 / (ringSum * 2); + return m_aPosHelper.clockwiseWedges() ? 360 - degAng : degAng; + } else { + /// The angle degree offset is set by the same property of the + /// data series. + /// Counter-clockwise offset from the 3 o'clock position. + return static_cast<double>(pSeries->getStartingAngle()); + } + }; + + m_aPosHelper.m_fAngleDegreeOffset = sAngle(); + + ///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_aPosHelper.isMathematicalOrientationRadius() && m_bUseRings ) + nExplodeableSlot = m_aZSlots.front().size()-1; - //iterate through all subsystems to create partial points + double fLogicYForNextPoint = 0.0; + ///iterate through all points to create shapes + for(sal_Int32 nPointIndex = 0; nPointIndex < nRingPtCnt; 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_aPosHelper.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 = pDataSrc->getData(pSeries, nPointIndex, eType); + 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 = + pDataSrc->getProps(pSeries, nPointIndex, eType); + + //iterate through all subsystems to create partial points + { + //logic values on angle axis: + double fLogicStartAngleValue = fLogicYPos / ringSum; + double fLogicEndAngleValue = (fLogicYPos+fLogicYValue) / ringSum; + + ///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 { - //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", "" ); - } + 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_aPosHelper.getWidthAngleDegree( fLogicStartAngleValue, fLogicEndAngleValue ); + aParam.mfUnitCircleStartAngleDegree = m_aPosHelper.transformToAngleDegree( fLogicStartAngleValue ); + aParam.mfUnitCircleInnerRadius = m_aPosHelper.transformToRadius( fLogicInnerRadius ); + aParam.mfUnitCircleOuterRadius = m_aPosHelper.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(eType, xSeriesGroupShape_Shapes, + xPointProperties, aParam, nRingPtCnt, + bConcentricExplosion); + + // Handle coloring of the composite wedge + sal_Int32 nPropIdx = propIndex(nPointIndex, eType, pDataSrc, + pSeries); + + ///point color: + if (!pSeries->hasPointOwnColor(nPropIdx) && m_xColorScheme.is()) + { + xPointShape->setPropertyValue("FillColor", + uno::Any(m_xColorScheme->getColorByIndex( nPropIdx ))); + } - ///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 ); - ///point color: - std::unique_ptr< tPropertyNameValueMap > apOverwritePropertiesMap; - if (!pSeries->hasPointOwnColor(nPointIndex) && m_xColorScheme.is()) + if(bHasFillColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nPropIdx, "FillColor"); + if(!std::isnan(nPropVal)) { - apOverwritePropertiesMap.reset( new tPropertyNameValueMap ); - (*apOverwritePropertiesMap)["FillColor"] <<= - m_xColorScheme->getColorByIndex( nPointIndex ); + xPointShape->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>( nPropVal))); } + } - ///create data point - aParam.mfLogicZ = -1.0; // For 3D pie chart label position - uno::Reference<drawing::XShape> xPointShape = - createDataPoint( - xSeriesGroupShape_Shapes, xPointProperties, apOverwritePropertiesMap.get(), aParam); + ///create label + createTextLabelShape(xTextTarget, *pSeries, nPropIdx, aParam); - if(bHasFillColorMapping) - { - double nPropVal = pSeries->getValueByProperty(nPointIndex, "FillColor"); - if(!std::isnan(nPropVal)) - { - uno::Reference< beans::XPropertySet > xProps( xPointShape, uno::UNO_QUERY_THROW ); - xProps->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>( nPropVal))); - } - } + if(!bDoExplode) + { + ShapeFactory::setShapeName( xPointShape + , ObjectIdentifier::createPointCID( + pSeries->getPointCID_Stub(), nPropIdx ) ); + } + 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_aPosHelper.transformUnitCircleToScene( fAngle, aParam.mfUnitCircleOuterRadius, aParam.mfLogicZ ); + drawing::Position3D aNewOrigin = m_aPosHelper.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, + nPropIdx ) ); + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + }//next series in x slot (next y slot) + }//next category +} - ///create label - createTextLabelShape(xTextTarget, *pSeries, nPointIndex, aParam); +void PieChart::createOneBar( + enum SubPieType eType, + ShapeParam& aParam, + const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget, + const rtl::Reference<SvxShapeGroup>& xTextTarget, + VDataSeries* pSeries, + const PieDataSrcBase *pDataSrc, + sal_Int32 n3DRelativeHeight) +{ + bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor"); - 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_pShapeFactory, m_nDimension ) ); - awt::Point aMaximumPosition( PlottingPositionHelper::transformSceneToScreenPosition( - aNewOrigin, m_xLogicTarget, m_pShapeFactory, 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 + sal_Int32 nBarPtCnt = pDataSrc->getNPoints(pSeries, eType); + + // Find sum of entries for this bar chart + double barSum = 0; + for (sal_Int32 nPointIndex = 0; nPointIndex < nBarPtCnt; nPointIndex++ ) { + double fY = pDataSrc->getData(pSeries, nPointIndex, eType); + if (!std::isnan(fY) ) barSum += fY; + } + + double fBarBottom = 0.0; + double fBarTop = -0.5; // make the bar go from -0.5 to 0.5 + ///iterate through all points to create shapes + for(sal_Int32 nPointIndex = 0; nPointIndex < nBarPtCnt; nPointIndex++ ) + { + aParam.mfDepth = getTransformedDepth() * (n3DRelativeHeight / 100.0); + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries, xSeriesTarget); + + ///collect data point information (logic coordinates, style ): + double fY = pDataSrc->getData(pSeries, nPointIndex, eType) / barSum; + if( std::isnan(fY) ) + continue; + if(fY==0.0)//@todo: continue also if the resolution is too small + continue; + fBarBottom = fBarTop; + fBarTop += fY; + + uno::Reference< beans::XPropertySet > xPointProperties = + pDataSrc->getProps(pSeries, nPointIndex, eType); + + ///create data point + aParam.mfLogicZ = -1.0; // For 3D pie chart label position + + rtl::Reference<SvxShape> xPointShape = + createBarDataPoint(xSeriesGroupShape_Shapes, + xPointProperties, aParam, + fBarBottom, fBarTop); + + sal_Int32 nPropIdx = propIndex(nPointIndex, eType, pDataSrc, pSeries); + + ///point color: + if (!pSeries->hasPointOwnColor(nPropIdx) && m_xColorScheme.is()) + { + xPointShape->setPropertyValue("FillColor", + uno::Any(m_xColorScheme->getColorByIndex( nPropIdx ))); + } + + + if(bHasFillColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nPropIdx, "FillColor"); + if(!std::isnan(nPropVal)) + { + xPointShape->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>( nPropVal))); + } + } + + ///create label + createTextLabelShape(xTextTarget, *pSeries, nPropIdx, aParam); + + ShapeFactory::setShapeName( xPointShape, + ObjectIdentifier::createPointCID( pSeries->getPointCID_Stub(), + nPropIdx ) ); + }//next category } PieChart::PieLabelInfo::PieLabelInfo() @@ -914,48 +1387,49 @@ bool PieChart::PieLabelInfo::moveAwayFrom( const PieChart::PieLabelInfo* pFix, c ///boxes (`aOverlap`). ::basegfx::B2IRectangle aOverlap( lcl_getRect( xLabelGroupShape ) ); aOverlap.intersect( lcl_getRect( pFix->xLabelGroupShape ) ); - if( !aOverlap.isEmpty() ) - { - //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; + 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; - xLabelGroupShape->setPosition( aNewAWTPos ); - bMoved = true; - } return true; ///note that no further test is performed in order to check that the @@ -1133,7 +1607,7 @@ bool PieChart::tryMoveLabels( PieLabelInfo const * pFirstBorder, PieLabelInfo co PieLabelInfo* p2 = pCenter->pNext; //return true when successful - bool bLabelOrderIsAntiClockWise = m_pPosHelper->isMathematicalOrientationAngle(); + bool bLabelOrderIsAntiClockWise = m_aPosHelper.isMathematicalOrientationAngle(); ///two loops are performed simultaneously: the outer loop iterates on ///`PieLabelInfo` objects in the list starting from the central element @@ -1243,7 +1717,7 @@ void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSi if(!bMoveableFound) return; - double fPageDiagonaleLength = sqrt( double(rPageSize.Width)*double(rPageSize.Width) + double(rPageSize.Height)*double(rPageSize.Height) ); + double fPageDiagonaleLength = std::hypot(rPageSize.Width, rPageSize.Height); if( fPageDiagonaleLength == 0.0 ) return; @@ -1273,20 +1747,11 @@ void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSi { if( labelInfo.bMoved && labelInfo.bShowLeaderLine ) { + const basegfx::B2IRectangle aRect(lcl_getRect(labelInfo.xLabelGroupShape)); 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(); + const sal_Int32 nX2 = std::clamp(nX1, aRect.getMinX(), aRect.getMaxX()); + const sal_Int32 nY2 = std::clamp(nY1, aRect.getMinY(), aRect.getMaxY()); //when the line is very short compared to the page size don't create one ::basegfx::B2DVector aLength(nX1-nX2, nY1-nY2); @@ -1295,15 +1760,14 @@ void PieChart::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& rPageSi drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } }; - uno::Reference< beans::XPropertySet > xProp( labelInfo.xTextShape, uno::UNO_QUERY); - if( xProp.is() ) + if( labelInfo.xTextShape.is() ) { sal_Int32 nColor = 0; - xProp->getPropertyValue("CharColor") >>= nColor; + labelInfo.xTextShape->SvxShape::getPropertyValue("CharColor") >>= nColor; if( nColor != -1 )//automatic font color does not work for lines -> fallback to black aVLineProperties.Color <<= nColor; } - m_pShapeFactory->createLine2D( labelInfo.xTextTarget, aPoints, &aVLineProperties ); + ShapeFactory::createLine2D( labelInfo.xTextTarget, aPoints, &aVLineProperties ); } } } @@ -1398,11 +1862,11 @@ bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam, PieLab // 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( + m_aPosHelper.transformUnitCircleToScene( fBisectingRayAngleDeg, rShapeParam.mfUnitCircleOuterRadius, fLogicZ ), - m_xLogicTarget, m_pShapeFactory, m_nDimension ); + m_xLogicTarget, m_nDimension ); // compute the pie radius basegfx::B2IVector aPieCenter = rPieLabelInfo.aOrigin; @@ -1484,8 +1948,7 @@ bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam, PieLab // compute the length of the diagonal vector d, // that is the distance between P and F - double fSquaredDistancePF = fDistancePM * fDistancePM + fOrthogonalEdgeLength * fOrthogonalEdgeLength; - double fDistancePF = sqrt( fSquaredDistancePF ); + double fDistancePF = std::hypot(fDistancePM, fOrthogonalEdgeLength); SAL_INFO( "chart2.pie.label.bestfit.inside", " width = " << fLabelWidth ); @@ -1695,6 +2158,92 @@ bool PieChart::performLabelBestFitInnerPlacement(ShapeParam& rShapeParam, PieLab return true; } +//======================= +// class PieDataSrc +//======================= +double PieDataSrc::getData(const VDataSeries* pSeries, sal_Int32 nPtIdx, + [[maybe_unused]] enum SubPieType eType) const +{ + return fabs(pSeries->getYValue( nPtIdx )); +} + +sal_Int32 PieDataSrc::getNPoints(const VDataSeries* pSeries, + [[maybe_unused]] enum SubPieType eType) const +{ + assert(eType == SubPieType::NONE); + return pSeries->getTotalPointCount(); +} + +uno::Reference< beans::XPropertySet > PieDataSrc::getProps( + const VDataSeries* pSeries, sal_Int32 nPtIdx, + [[maybe_unused]] enum SubPieType eType) const +{ + assert(eType == SubPieType::NONE); + return pSeries->getPropertiesOfPoint(nPtIdx); +} + + +//======================= +// class OfPieDataSrc +//======================= + +// For now, just implement the default Excel behavior, which is that the +// right pie consists of the last three entries in the series. Other +// behaviors should be supported later. +// TODO + +sal_Int32 OfPieDataSrc::getNPoints(const VDataSeries* pSeries, + enum SubPieType eType) const +{ + if (eType == SubPieType::LEFT) { + return pSeries->getTotalPointCount() - 2; + } else { + assert(eType == SubPieType::RIGHT); + return 3; + } +} + +double OfPieDataSrc::getData(const VDataSeries* pSeries, sal_Int32 nPtIdx, + enum SubPieType eType) const +{ + const sal_Int32 n = pSeries->getTotalPointCount() - 3; + if (eType == SubPieType::LEFT) { + // nPtIdx should be in [0, n] + if (nPtIdx < n) { + return fabs(pSeries->getYValue( nPtIdx )); + } else { + assert(nPtIdx == n); + return fabs(pSeries->getYValue(n)) + + fabs(pSeries->getYValue(n+1)) + + fabs(pSeries->getYValue(n+2)); + } + } else { + assert(eType == SubPieType::RIGHT); + return fabs(pSeries->getYValue(nPtIdx + n)); + } +} + +uno::Reference< beans::XPropertySet > OfPieDataSrc::getProps( + const VDataSeries* pSeries, sal_Int32 nPtIdx, + enum SubPieType eType) const +{ + const sal_Int32 nPts = pSeries->getTotalPointCount(); + const sal_Int32 n = nPts - 3; + if (eType == SubPieType::LEFT) { + // nPtIdx should be in [0, n] + if (nPtIdx < n) { + return pSeries->getPropertiesOfPoint( nPtIdx ); + } else { + // The aggregated wedge + assert(nPtIdx == n); + return pSeries->getPropertiesOfPoint(nPts); + } + } else { + assert(eType == SubPieType::RIGHT); + return pSeries->getPropertiesOfPoint(nPtIdx + n); + } +} + } //namespace chart /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |