diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /chart2/source/view/charttypes | |
parent | Initial commit. (diff) | |
download | libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'chart2/source/view/charttypes')
20 files changed, 9599 insertions, 0 deletions
diff --git a/chart2/source/view/charttypes/AreaChart.cxx b/chart2/source/view/charttypes/AreaChart.cxx new file mode 100644 index 000000000..9cb2e06ba --- /dev/null +++ b/chart2/source/view/charttypes/AreaChart.cxx @@ -0,0 +1,952 @@ +/* -*- 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 "AreaChart.hxx" +#include <PlottingPositionHelper.hxx> +#include <ShapeFactory.hxx> +#include <CommonConverters.hxx> +#include <ExplicitCategoriesProvider.hxx> +#include <ObjectIdentifier.hxx> +#include "Splines.hxx" +#include <ChartType.hxx> +#include <ChartTypeHelper.hxx> +#include <LabelPositionHelper.hxx> +#include <Clipping.hxx> +#include <Stripe.hxx> +#include <DateHelper.hxx> +#include <unonames.hxx> +#include <ConfigAccess.hxx> + +#include <com/sun/star/chart2/Symbol.hpp> +#include <com/sun/star/chart/DataLabelPlacement.hpp> +#include <com/sun/star/chart/MissingValueTreatment.hpp> + +#include <sal/log.hxx> +#include <o3tl/safeint.hxx> +#include <osl/diagnose.h> + +#include <com/sun/star/beans/XPropertySet.hpp> +#include <officecfg/Office/Compatibility.hxx> + +#include <limits> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +AreaChart::AreaChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bCategoryXAxis + , bool bNoArea + ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount, bCategoryXAxis ) + , m_pMainPosHelper(new PlottingPositionHelper()) + , m_bArea(!bNoArea) + , m_bLine(bNoArea) + , m_bSymbol( ChartTypeHelper::isSupportingSymbolProperties(xChartTypeModel,nDimensionCount) ) + , m_eCurveStyle(CurveStyle_LINES) + , m_nCurveResolution(20) + , m_nSplineOrder(3) +{ + m_pMainPosHelper->AllowShiftXAxisPos(true); + m_pMainPosHelper->AllowShiftZAxisPos(true); + + PlotterBase::m_pPosHelper = m_pMainPosHelper.get(); + VSeriesPlotter::m_pMainPosHelper = m_pMainPosHelper.get(); + + try + { + if( m_xChartTypeModel.is() ) + { + m_xChartTypeModel->getPropertyValue(CHART_UNONAME_CURVE_STYLE) >>= m_eCurveStyle; + m_xChartTypeModel->getPropertyValue(CHART_UNONAME_CURVE_RESOLUTION) >>= m_nCurveResolution; + m_xChartTypeModel->getPropertyValue(CHART_UNONAME_SPLINE_ORDER) >>= m_nSplineOrder; + } + } + catch( uno::Exception& e ) + { + //the above properties are not supported by all charttypes supported by this class (e.g. area or net chart) + //in that cases this exception is ok + e.Context.is();//to have debug information without compilation warnings + } +} + +AreaChart::~AreaChart() +{ +} + +bool AreaChart::isSeparateStackingForDifferentSigns( sal_Int32 /*nDimensionIndex*/ ) +{ + // no separate stacking in all types of line/area charts + return false; +} + +LegendSymbolStyle AreaChart::getLegendSymbolStyle() +{ + if( m_bArea || m_nDimension == 3 ) + return LegendSymbolStyle::Box; + return LegendSymbolStyle::Line; +} + +uno::Any AreaChart::getExplicitSymbol( const VDataSeries& rSeries, sal_Int32 nPointIndex ) +{ + uno::Any aRet; + + Symbol* pSymbolProperties = rSeries.getSymbolProperties( nPointIndex ); + if( pSymbolProperties ) + { + aRet <<= *pSymbolProperties; + } + + return aRet; +} + +drawing::Direction3D AreaChart::getPreferredDiagramAspectRatio() const +{ + drawing::Direction3D aRet(1,-1,1); + if( m_nDimension == 2 ) + aRet = drawing::Direction3D(-1,-1,-1); + else if (m_pPosHelper) + { + drawing::Direction3D aScale( m_pPosHelper->getScaledLogicWidth() ); + aRet.DirectionZ = aScale.DirectionZ*0.2; + if(aRet.DirectionZ>1.0) + aRet.DirectionZ=1.0; + if(aRet.DirectionZ>10) + aRet.DirectionZ=10; + } + return aRet; +} + +void AreaChart::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) +{ + if( m_bArea && pSeries ) + { + sal_Int32 nMissingValueTreatment = pSeries->getMissingValueTreatment(); + if( nMissingValueTreatment == css::chart::MissingValueTreatment::LEAVE_GAP ) + pSeries->setMissingValueTreatment( css::chart::MissingValueTreatment::USE_ZERO ); + } + if( m_nDimension == 3 && !m_bCategoryXAxis ) + { + //3D xy always deep + OSL_ENSURE( zSlot==-1,"3D xy charts should be deep stacked in model also" ); + zSlot=-1; + xSlot=0; + ySlot=0; + } + VSeriesPlotter::addSeries( std::move(pSeries), zSlot, xSlot, ySlot ); +} + +static void lcl_removeDuplicatePoints( std::vector<std::vector<css::drawing::Position3D>>& rPolyPoly, PlottingPositionHelper& rPosHelper ) +{ + sal_Int32 nPolyCount = rPolyPoly.size(); + if(!nPolyCount) + return; + + // TODO we could do with without a temporary array + std::vector<std::vector<css::drawing::Position3D>> aTmp; + aTmp.resize(nPolyCount); + + for( sal_Int32 nPolygonIndex = 0; nPolygonIndex<nPolyCount; nPolygonIndex++ ) + { + std::vector<css::drawing::Position3D>* pOuterSource = &rPolyPoly[nPolygonIndex]; + std::vector<css::drawing::Position3D>* pOuterTarget = &aTmp[nPolygonIndex]; + + sal_Int32 nPointCount = pOuterSource->size(); + if( !nPointCount ) + continue; + + pOuterTarget->resize(nPointCount); + + css::drawing::Position3D* pSource = pOuterSource->data(); + css::drawing::Position3D* pTarget = pOuterTarget->data(); + + //copy first point + *pTarget=*pSource++; + sal_Int32 nTargetPointCount=1; + + for( sal_Int32 nSource=1; nSource<nPointCount; nSource++ ) + { + if( !rPosHelper.isSameForGivenResolution( pTarget->PositionX, pTarget->PositionY, pTarget->PositionZ + , pSource->PositionX, pSource->PositionY, pSource->PositionZ ) ) + { + pTarget++; + *pTarget=*pSource; + nTargetPointCount++; + } + pSource++; + } + + //free unused space + if( nTargetPointCount<nPointCount ) + { + pOuterTarget->resize(nTargetPointCount); + } + + pOuterSource->clear(); + } + + //free space + rPolyPoly.resize(nPolyCount); + + rPolyPoly = std::move(aTmp); +} + +bool AreaChart::create_stepped_line( + std::vector<std::vector<css::drawing::Position3D>> aStartPoly, + chart2::CurveStyle eCurveStyle, + PlottingPositionHelper const * pPosHelper, + std::vector<std::vector<css::drawing::Position3D>> &aPoly ) +{ + sal_uInt32 nOuterCount = aStartPoly.size(); + if ( !nOuterCount ) + return false; + + std::vector<std::vector<css::drawing::Position3D>> aSteppedPoly; + aSteppedPoly.resize(nOuterCount); + + auto pSequence = aSteppedPoly.data(); + + for( sal_uInt32 nOuter = 0; nOuter < nOuterCount; ++nOuter ) + { + if( aStartPoly[nOuter].size() <= 1 ) + continue; //we need at least two points + + sal_uInt32 nMaxIndexPoints = aStartPoly[nOuter].size()-1; // is >1 + sal_uInt32 nNewIndexPoints = 0; + if ( eCurveStyle==CurveStyle_STEP_START || eCurveStyle==CurveStyle_STEP_END) + nNewIndexPoints = nMaxIndexPoints * 2 + 1; + else + nNewIndexPoints = nMaxIndexPoints * 3 + 1; + + const css::drawing::Position3D* pOld = aStartPoly[nOuter].data(); + + pSequence[nOuter].resize( nNewIndexPoints ); + + css::drawing::Position3D* pNew = pSequence[nOuter].data(); + + pNew[0] = pOld[0]; + for( sal_uInt32 oi = 0; oi < nMaxIndexPoints; oi++ ) + { + switch ( eCurveStyle ) + { + case CurveStyle_STEP_START: + /** O + | + | + | + O-----+ + */ + // create the intermediate point + pNew[1+oi*2].PositionX = pOld[oi+1].PositionX; + pNew[1+oi*2].PositionY = pOld[oi].PositionY; + pNew[1+oi*2].PositionZ = pOld[oi].PositionZ; + // and now the normal one + pNew[1+oi*2+1] = pOld[oi+1]; + break; + case CurveStyle_STEP_END: + /** +------O + | + | + | + O + */ + // create the intermediate point + pNew[1+oi*2].PositionX = pOld[oi].PositionX; + pNew[1+oi*2].PositionY = pOld[oi+1].PositionY; + pNew[1+oi*2].PositionZ = pOld[oi].PositionZ; + // and now the normal one + pNew[1+oi*2+1] = pOld[oi+1]; + break; + case CurveStyle_STEP_CENTER_X: + /** +--O + | + | + | + O--+ + */ + // create the first intermediate point + pNew[1+oi*3].PositionX = (pOld[oi].PositionX + pOld[oi+1].PositionX) / 2; + pNew[1+oi*3].PositionY = pOld[oi].PositionY; + pNew[1+oi*3].PositionZ = pOld[oi].PositionZ; + // create the second intermediate point + pNew[1+oi*3+1].PositionX = (pOld[oi].PositionX + pOld[oi+1].PositionX) / 2; + pNew[1+oi*3+1].PositionY = pOld[oi+1].PositionY; + pNew[1+oi*3+1].PositionZ = pOld[oi].PositionZ; + // and now the normal one + pNew[1+oi*3+2] = pOld[oi+1]; + break; + case CurveStyle_STEP_CENTER_Y: + /** O + | + +-----+ + | + O + */ + // create the first intermediate point + pNew[1+oi*3].PositionX = pOld[oi].PositionX; + pNew[1+oi*3].PositionY = (pOld[oi].PositionY + pOld[oi+1].PositionY) / 2; + pNew[1+oi*3].PositionZ = pOld[oi].PositionZ; + // create the second intermediate point + pNew[1+oi*3+1].PositionX = pOld[oi+1].PositionX; + pNew[1+oi*3+1].PositionY = (pOld[oi].PositionY + pOld[oi+1].PositionY) / 2; + pNew[1+oi*3+1].PositionZ = pOld[oi].PositionZ; + // and now the normal one + pNew[1+oi*3+2] = pOld[oi+1]; + break; + default: + // this should never be executed + OSL_FAIL("Unknown curvestyle in AreaChart::create_stepped_line"); + } + } + } + Clipping::clipPolygonAtRectangle( aSteppedPoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + + return true; +} + +bool AreaChart::impl_createLine( VDataSeries* pSeries + , std::vector<std::vector<css::drawing::Position3D>> const * pSeriesPoly + , PlottingPositionHelper* pPosHelper ) +{ + //return true if a line was created successfully + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeBackChild(pSeries, m_xSeriesTarget); + + std::vector<std::vector<css::drawing::Position3D>> aPoly; + if(m_eCurveStyle==CurveStyle_CUBIC_SPLINES) + { + std::vector<std::vector<css::drawing::Position3D>> aSplinePoly; + SplineCalculater::CalculateCubicSplines( *pSeriesPoly, aSplinePoly, m_nCurveResolution ); + lcl_removeDuplicatePoints( aSplinePoly, *pPosHelper ); + Clipping::clipPolygonAtRectangle( aSplinePoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + } + else if(m_eCurveStyle==CurveStyle_B_SPLINES) + { + std::vector<std::vector<css::drawing::Position3D>> aSplinePoly; + SplineCalculater::CalculateBSplines( *pSeriesPoly, aSplinePoly, m_nCurveResolution, m_nSplineOrder ); + lcl_removeDuplicatePoints( aSplinePoly, *pPosHelper ); + Clipping::clipPolygonAtRectangle( aSplinePoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + } + else if (m_eCurveStyle==CurveStyle_STEP_START || + m_eCurveStyle==CurveStyle_STEP_END || + m_eCurveStyle==CurveStyle_STEP_CENTER_Y || + m_eCurveStyle==CurveStyle_STEP_CENTER_X + ) + { + if (!create_stepped_line(*pSeriesPoly, m_eCurveStyle, pPosHelper, aPoly)) + { + return false; + } + } + else + { // default to creating a straight line + SAL_WARN_IF(m_eCurveStyle != CurveStyle_LINES, "chart2.areachart", "Unknown curve style"); + Clipping::clipPolygonAtRectangle( *pSeriesPoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + } + + if(!ShapeFactory::hasPolygonAnyLines(aPoly)) + return false; + + //transformation 3) -> 4) + pPosHelper->transformScaledLogicToScene( aPoly ); + + //create line: + rtl::Reference< SvxShape > xShape; + if(m_nDimension==3) + { + double fDepth = getTransformedDepth(); + sal_Int32 nPolyCount = aPoly.size(); + for(sal_Int32 nPoly=0;nPoly<nPolyCount;nPoly++) + { + sal_Int32 nPointCount = aPoly[nPoly].size(); + for(sal_Int32 nPoint=0;nPoint<nPointCount-1;nPoint++) + { + drawing::Position3D aPoint1, aPoint2; + aPoint1 = aPoly[nPoly][nPoint+1]; + aPoint2 = aPoly[nPoly][nPoint]; + + ShapeFactory::createStripe(xSeriesGroupShape_Shapes + , Stripe( aPoint1, aPoint2, fDepth ) + , pSeries->getPropertiesOfSeries(), PropertyMapper::getPropertyNameMapForFilledSeriesProperties(), true, 1 ); + } + } + } + else //m_nDimension!=3 + { + xShape = ShapeFactory::createLine2D( xSeriesGroupShape_Shapes, aPoly ); + PropertyMapper::setMappedProperties( *xShape + , pSeries->getPropertiesOfSeries() + , PropertyMapper::getPropertyNameMapForLineSeriesProperties() ); + //because of this name this line will be used for marking + ::chart::ShapeFactory::setShapeName(xShape, "MarkHandles"); + } + return true; +} + +bool AreaChart::impl_createArea( VDataSeries* pSeries + , std::vector<std::vector<css::drawing::Position3D>> const * pSeriesPoly + , std::vector<std::vector<css::drawing::Position3D>> const * pPreviousSeriesPoly + , PlottingPositionHelper const * pPosHelper ) +{ + //return true if an area was created successfully + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeBackChild(pSeries, m_xSeriesTarget); + double zValue = pSeries->m_fLogicZPos; + + std::vector<std::vector<css::drawing::Position3D>> aPoly( *pSeriesPoly ); + //add second part to the polygon (grounding points or previous series points) + if(!pPreviousSeriesPoly) + { + double fMinX = pSeries->m_fLogicMinX; + double fMaxX = pSeries->m_fLogicMaxX; + double fY = pPosHelper->getBaseValueY();//logic grounding + if( m_nDimension==3 ) + fY = pPosHelper->getLogicMinY(); + + //clip to scale + if(fMaxX<pPosHelper->getLogicMinX() || fMinX>pPosHelper->getLogicMaxX()) + return false;//no visible shape needed + pPosHelper->clipLogicValues( &fMinX, &fY, nullptr ); + pPosHelper->clipLogicValues( &fMaxX, nullptr, nullptr ); + + //apply scaling + { + pPosHelper->doLogicScaling( &fMinX, &fY, &zValue ); + pPosHelper->doLogicScaling( &fMaxX, nullptr, nullptr ); + } + + AddPointToPoly( aPoly, drawing::Position3D( fMaxX,fY,zValue) ); + AddPointToPoly( aPoly, drawing::Position3D( fMinX,fY,zValue) ); + } + else + { + appendPoly( aPoly, *pPreviousSeriesPoly ); + } + ShapeFactory::closePolygon(aPoly); + + //apply clipping + { + std::vector<std::vector<css::drawing::Position3D>> aClippedPoly; + Clipping::clipPolygonAtRectangle( aPoly, pPosHelper->getScaledLogicClipDoubleRect(), aClippedPoly, false ); + ShapeFactory::closePolygon(aClippedPoly); //again necessary after clipping + aPoly = aClippedPoly; + } + + if(!ShapeFactory::hasPolygonAnyLines(aPoly)) + return false; + + //transformation 3) -> 4) + pPosHelper->transformScaledLogicToScene( aPoly ); + + //create area: + rtl::Reference< SvxShape > xShape; + if(m_nDimension==3) + { + xShape = ShapeFactory::createArea3D( xSeriesGroupShape_Shapes + , aPoly, getTransformedDepth() ); + } + else //m_nDimension!=3 + { + xShape = ShapeFactory::createArea2D( xSeriesGroupShape_Shapes + , aPoly ); + } + PropertyMapper::setMappedProperties( *xShape + , pSeries->getPropertiesOfSeries() + , PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + //because of this name this line will be used for marking + ::chart::ShapeFactory::setShapeName(xShape, "MarkHandles"); + return true; +} + +void AreaChart::impl_createSeriesShapes() +{ + //the polygon shapes for each series need to be created before + + //iterate through all series again to create the series shapes + for( auto const& rZSlot : m_aZSlots ) + { + for( auto const& rXSlot : rZSlot ) + { + std::map< sal_Int32, std::vector<std::vector<css::drawing::Position3D>>* > aPreviousSeriesPolyMap;//a PreviousSeriesPoly for each different nAttachedAxisIndex + std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly = nullptr; + + //iterate through all series + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + PlottingPositionHelper& rPosHelper = getPlottingPositionHelper(nAttachedAxisIndex); + m_pPosHelper = &rPosHelper; + + createRegressionCurvesShapes( *pSeries, m_xErrorBarTarget, m_xRegressionCurveEquationTarget, + m_pPosHelper->maySkipPointsInRegressionCalculation()); + + pSeriesPoly = &pSeries->m_aPolyPolygonShape3D; + if( m_bArea ) + { + if (!impl_createArea(pSeries.get(), pSeriesPoly, + aPreviousSeriesPolyMap[nAttachedAxisIndex], &rPosHelper)) + continue; + } + if( m_bLine ) + { + if (!impl_createLine(pSeries.get(), pSeriesPoly, &rPosHelper)) + continue; + } + aPreviousSeriesPolyMap[nAttachedAxisIndex] = pSeriesPoly; + }//next series in x slot (next y slot) + }//next x slot + }//next z slot +} + +namespace +{ + +void lcl_reorderSeries( std::vector< std::vector< VDataSeriesGroup > >& rZSlots ) +{ + std::vector< std::vector< VDataSeriesGroup > > aRet; + aRet.reserve( rZSlots.size() ); + + std::vector< std::vector< VDataSeriesGroup > >::reverse_iterator aZIt( rZSlots.rbegin() ); + std::vector< std::vector< VDataSeriesGroup > >::reverse_iterator aZEnd( rZSlots.rend() ); + for( ; aZIt != aZEnd; ++aZIt ) + { + std::vector< VDataSeriesGroup > aXSlot; + aXSlot.reserve( aZIt->size() ); + + std::vector< VDataSeriesGroup >::reverse_iterator aXIt( aZIt->rbegin() ); + std::vector< VDataSeriesGroup >::reverse_iterator aXEnd( aZIt->rend() ); + for( ; aXIt != aXEnd; ++aXIt ) + aXSlot.push_back(std::move(*aXIt)); + + aRet.push_back(std::move(aXSlot)); + } + + rZSlots = std::move(aRet); +} + +//better performance for big data +struct FormerPoint +{ + FormerPoint( double fX, double fY, double fZ ) + : m_fX(fX), m_fY(fY), m_fZ(fZ) + {} + FormerPoint() + : m_fX(std::numeric_limits<double>::quiet_NaN()) + , m_fY(std::numeric_limits<double>::quiet_NaN()) + , m_fZ(std::numeric_limits<double>::quiet_NaN()) + { + } + + double m_fX; + double m_fY; + double m_fZ; +}; + +}//anonymous namespace + +void AreaChart::createShapes() +{ + if( m_aZSlots.empty() ) //no series + return; + + //tdf#127813 Don't reverse the series in OOXML-heavy environments + if( officecfg::Office::Compatibility::View::ReverseSeriesOrderAreaAndNetChart::get() && m_nDimension == 2 && ( m_bArea || !m_bCategoryXAxis ) ) + lcl_reorderSeries( m_aZSlots ); + + OSL_ENSURE(m_xLogicTarget.is()&&m_xFinalTarget.is(),"AreaChart is not proper initialized"); + if(!(m_xLogicTarget.is()&&m_xFinalTarget.is())) + return; + + //the text labels should be always on top of the other series shapes + //for area chart the error bars should be always on top of the other series shapes + + //therefore create an own group for the texts and the error bars to move them to front + //(because the text group is created after the series group the texts are displayed on top) + m_xSeriesTarget = createGroupShape( m_xLogicTarget ); + if( m_bArea ) + m_xErrorBarTarget = createGroupShape( m_xLogicTarget ); + else + m_xErrorBarTarget = m_xSeriesTarget; + m_xTextTarget = ShapeFactory::createGroup2D( m_xFinalTarget ); + m_xRegressionCurveEquationTarget = ShapeFactory::createGroup2D( m_xFinalTarget ); + + //check necessary here that different Y axis can not be stacked in the same group? ... hm? + + //update/create information for current group + double fLogicZ = 1.0;//as defined + + sal_Int32 nStartIndex = 0; // inclusive ;..todo get somehow from x scale + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + if(nEndIndex<=0) + nEndIndex=1; + + //better performance for big data + std::map< VDataSeries*, FormerPoint > aSeriesFormerPointMap; + m_bPointsWereSkipped = false; + sal_Int32 nSkippedPoints = 0; + sal_Int32 nCreatedPoints = 0; + + bool bDateCategory = (m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis()); + + std::vector<std::map< sal_Int32, double > > aLogicYSumMapByX(nEndIndex);//one for each different nAttachedAxisIndex + for( auto const& rZSlot : m_aZSlots ) + { + //iterate through all x slots in this category to get 100percent sum + for( auto const& rXSlot : rZSlot ) + { + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + if (bDateCategory) + pSeries->doSortByXValues(); + + for( sal_Int32 nIndex = nStartIndex; nIndex < nEndIndex; nIndex++ ) + { + std::map< sal_Int32, double >& rLogicYSumMap = aLogicYSumMapByX[nIndex]; + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + rLogicYSumMap.insert({nAttachedAxisIndex, 0.0}); + + m_pPosHelper = &getPlottingPositionHelper(nAttachedAxisIndex); + + double fAdd = pSeries->getYValue( nIndex ); + if( !std::isnan(fAdd) && !std::isinf(fAdd) ) + rLogicYSumMap[nAttachedAxisIndex] += fabs( fAdd ); + } + } + } + } + + const bool bUseErrorRectangle = ConfigAccess::getUseErrorRectangle(); + + sal_Int32 nZ=1; + for( auto const& rZSlot : m_aZSlots ) + { + //for the area chart there should be at most one x slot (no side by side stacking available) + //attention different: xSlots are always interpreted as independent areas one behind the other: @todo this doesn't work why not??? + for( auto const& rXSlot : rZSlot ) + { + std::vector<std::map< sal_Int32, double > > aLogicYForNextSeriesMapByX(nEndIndex); //one for each different nAttachedAxisIndex + //iterate through all series + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeFrontChild(pSeries.get(), m_xSeriesTarget); + + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + double fXMin, fXMax; + pSeries->getMinMaxXValue(fXMin, fXMax); + PlottingPositionHelper& rPosHelper = getPlottingPositionHelper(nAttachedAxisIndex); + m_pPosHelper = &rPosHelper; + + if(m_nDimension==3) + fLogicZ = nZ+0.5; + pSeries->m_fLogicZPos = fLogicZ; + + for( sal_Int32 nIndex = nStartIndex; nIndex < nEndIndex; nIndex++ ) + { + + /* #i70133# ignore points outside of series length in standard area + charts. Stacked area charts will use missing points as zeros. In + standard charts, pSeriesList contains only one series. */ + if( m_bArea && (rXSlot.m_aSeriesVector.size() == 1) && (nIndex >= pSeries->getTotalPointCount()) ) + continue; + + //collect data point information (logic coordinates, style ): + double fLogicX = pSeries->getXValue(nIndex); + if (bDateCategory) + { + if (std::isnan(fLogicX) || (fLogicX < fXMin || fLogicX > fXMax)) + continue; + + fLogicX = DateHelper::RasterizeDateValue( fLogicX, m_aNullDate, m_nTimeResolution ); + } + double fLogicY = pSeries->getYValue(nIndex); + + if( m_nDimension==3 && m_bArea && rXSlot.m_aSeriesVector.size()!=1 ) + fLogicY = fabs( fLogicY ); + + double fLogicValueForLabeDisplay = fLogicY; + std::map< sal_Int32, double >& rLogicYSumMap = aLogicYSumMapByX[nIndex]; + if (rPosHelper.isPercentY() && rLogicYSumMap[nAttachedAxisIndex] != 0.0) + { + fLogicY = fabs( fLogicY )/rLogicYSumMap[nAttachedAxisIndex]; + } + + if( std::isnan(fLogicX) || std::isinf(fLogicX) + || std::isnan(fLogicY) || std::isinf(fLogicY) + || std::isnan(fLogicZ) || std::isinf(fLogicZ) ) + { + if( pSeries->getMissingValueTreatment() == css::chart::MissingValueTreatment::LEAVE_GAP ) + { + std::vector<std::vector<css::drawing::Position3D>>& rPolygon = pSeries->m_aPolyPolygonShape3D; + sal_Int32& rIndex = pSeries->m_nPolygonIndex; + if( 0<= rIndex && o3tl::make_unsigned(rIndex) < rPolygon.size() ) + { + if( !rPolygon[ rIndex ].empty() ) + rIndex++; //start a new polygon for the next point if the current poly is not empty + } + } + continue; + } + + std::map< sal_Int32, double >& rLogicYForNextSeriesMap = aLogicYForNextSeriesMapByX[nIndex]; + rLogicYForNextSeriesMap.try_emplace(nAttachedAxisIndex, 0.0); + + double fPreviousYValue = rLogicYForNextSeriesMap[nAttachedAxisIndex]; + fLogicY += rLogicYForNextSeriesMap[nAttachedAxisIndex]; + rLogicYForNextSeriesMap[nAttachedAxisIndex] = fLogicY; + + bool bIsVisible = rPosHelper.isLogicVisible(fLogicX, fLogicY, fLogicZ); + + //remind minimal and maximal x values for area 'grounding' points + //only for filled area + { + double& rfMinX = pSeries->m_fLogicMinX; + if(!nIndex||fLogicX<rfMinX) + rfMinX=fLogicX; + double& rfMaxX = pSeries->m_fLogicMaxX; + if(!nIndex||fLogicX>rfMaxX) + rfMaxX=fLogicX; + } + + drawing::Position3D aUnscaledLogicPosition( fLogicX, fLogicY, fLogicZ ); + drawing::Position3D aScaledLogicPosition(aUnscaledLogicPosition); + rPosHelper.doLogicScaling(aScaledLogicPosition); + + //transformation 3) -> 4) + drawing::Position3D aScenePosition( + rPosHelper.transformLogicToScene(fLogicX, fLogicY, fLogicZ, false)); + + //better performance for big data + FormerPoint aFormerPoint( aSeriesFormerPointMap[pSeries.get()] ); + rPosHelper.setCoordinateSystemResolution(m_aCoordinateSystemResolution); + if (!pSeries->isAttributedDataPoint(nIndex) + && rPosHelper.isSameForGivenResolution( + aFormerPoint.m_fX, aFormerPoint.m_fY, aFormerPoint.m_fZ, + aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, + aScaledLogicPosition.PositionZ)) + { + ++nSkippedPoints; + m_bPointsWereSkipped = true; + continue; + } + aSeriesFormerPointMap[pSeries.get()] = FormerPoint(aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, aScaledLogicPosition.PositionZ); + + //store point information for series polygon + //for area and/or line (symbols only do not need this) + if( isValidPosition(aScaledLogicPosition) ) + { + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aScaledLogicPosition, pSeries->m_nPolygonIndex ); + } + + //create a single datapoint if point is visible + //apply clipping: + if( !bIsVisible ) + continue; + + bool bCreateYErrorBar = false, bCreateXErrorBar = false; + { + uno::Reference< beans::XPropertySet > xErrorBarProp(pSeries->getYErrorBarProperties(nIndex)); + if( xErrorBarProp.is() ) + { + bool bShowPositive = false; + bool bShowNegative = false; + xErrorBarProp->getPropertyValue("ShowPositiveError") >>= bShowPositive; + xErrorBarProp->getPropertyValue("ShowNegativeError") >>= bShowNegative; + bCreateYErrorBar = bShowPositive || bShowNegative; + } + + xErrorBarProp = pSeries->getXErrorBarProperties(nIndex); + if ( xErrorBarProp.is() ) + { + bool bShowPositive = false; + bool bShowNegative = false; + xErrorBarProp->getPropertyValue("ShowPositiveError") >>= bShowPositive; + xErrorBarProp->getPropertyValue("ShowNegativeError") >>= bShowNegative; + bCreateXErrorBar = bShowPositive || bShowNegative; + } + } + + Symbol* pSymbolProperties = m_bSymbol ? pSeries->getSymbolProperties( nIndex ) : nullptr; + bool bCreateSymbol = pSymbolProperties && (pSymbolProperties->Style != SymbolStyle_NONE); + + if( !bCreateSymbol && !bCreateYErrorBar && + !bCreateXErrorBar && !pSeries->getDataPointLabelIfLabel(nIndex) ) + continue; + + { + nCreatedPoints++; + + //create data point + drawing::Direction3D aSymbolSize(0,0,0); + if( bCreateSymbol ) + { + if(m_nDimension!=3) + { + //create a group shape for this point and add to the series shape: + OUString aPointCID = ObjectIdentifier::createPointCID( + pSeries->getPointCID_Stub(), nIndex ); + rtl::Reference<SvxShapeGroupAnyD> xPointGroupShape_Shapes; + if (pSymbolProperties->Style == SymbolStyle_STANDARD || pSymbolProperties->Style == SymbolStyle_GRAPHIC) + xPointGroupShape_Shapes = createGroupShape(xSeriesGroupShape_Shapes,aPointCID); + + if (pSymbolProperties->Style != SymbolStyle_NONE) + { + aSymbolSize.DirectionX = pSymbolProperties->Size.Width; + aSymbolSize.DirectionY = pSymbolProperties->Size.Height; + } + + if (pSymbolProperties->Style == SymbolStyle_STANDARD) + { + sal_Int32 nSymbol = pSymbolProperties->StandardSymbol; + ShapeFactory::createSymbol2D( + xPointGroupShape_Shapes, aScenePosition, aSymbolSize, + nSymbol, pSymbolProperties->BorderColor, + pSymbolProperties->FillColor); + } + else if (pSymbolProperties->Style == SymbolStyle_GRAPHIC) + { + ShapeFactory::createGraphic2D(xPointGroupShape_Shapes, + aScenePosition, aSymbolSize, + pSymbolProperties->Graphic); + } + //@todo other symbol styles + } + } + //create error bars or rectangles, depending on configuration + if ( bUseErrorRectangle ) + { + if ( bCreateXErrorBar || bCreateYErrorBar ) + { + createErrorRectangle( + aUnscaledLogicPosition, + *pSeries, + nIndex, + m_xErrorBarTarget, + bCreateXErrorBar, + bCreateYErrorBar ); + } + } + else + { + if (bCreateXErrorBar) + createErrorBar_X( aUnscaledLogicPosition, *pSeries, nIndex, m_xErrorBarTarget ); + + if (bCreateYErrorBar) + createErrorBar_Y( aUnscaledLogicPosition, *pSeries, nIndex, m_xErrorBarTarget, nullptr ); + } + + //create data point label + if( pSeries->getDataPointLabelIfLabel(nIndex) ) + { + LabelAlignment eAlignment = LABEL_ALIGN_TOP; + sal_Int32 nLabelPlacement = pSeries->getLabelPlacement( + nIndex, m_xChartTypeModel, rPosHelper.isSwapXAndY()); + + if (m_bArea && nLabelPlacement == css::chart::DataLabelPlacement::CENTER) + { + if (fPreviousYValue) + fLogicY -= (fLogicY - fPreviousYValue) / 2.0; + else + fLogicY = (fLogicY + rPosHelper.getLogicMinY()) / 2.0; + aScenePosition = rPosHelper.transformLogicToScene(fLogicX, fLogicY, fLogicZ, false); + } + + drawing::Position3D aScenePosition3D( aScenePosition.PositionX + , aScenePosition.PositionY + , aScenePosition.PositionZ+getTransformedDepth() ); + + switch(nLabelPlacement) + { + case css::chart::DataLabelPlacement::TOP: + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + case css::chart::DataLabelPlacement::BOTTOM: + aScenePosition3D.PositionY += (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_BOTTOM; + break; + case css::chart::DataLabelPlacement::LEFT: + aScenePosition3D.PositionX -= (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_LEFT; + break; + case css::chart::DataLabelPlacement::RIGHT: + aScenePosition3D.PositionX += (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_RIGHT; + break; + case css::chart::DataLabelPlacement::CENTER: + eAlignment = LABEL_ALIGN_CENTER; + break; + default: + OSL_FAIL("this label alignment is not implemented yet"); + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + } + + awt::Point aScreenPosition2D;//get the screen position for the labels + sal_Int32 nOffset = 100; //todo maybe calculate this font height dependent + { + if(eAlignment==LABEL_ALIGN_CENTER || m_nDimension == 3 ) + nOffset = 0; + aScreenPosition2D = LabelPositionHelper(m_nDimension,m_xLogicTarget) + .transformSceneToScreenPosition( aScenePosition3D ); + } + + createDataLabel( m_xTextTarget, *pSeries, nIndex + , fLogicValueForLabeDisplay + , rLogicYSumMap[nAttachedAxisIndex], aScreenPosition2D, eAlignment, nOffset ); + } + } + } + + }//next series in x slot (next y slot) + }//next x slot + ++nZ; + }//next z slot + + impl_createSeriesShapes(); + + /* @todo remove series shapes if empty + //remove and delete point-group-shape if empty + if(!xSeriesGroupShape_Shapes->getCount()) + { + pSeries->m_xShape.set(NULL); + m_xLogicTarget->remove(xSeriesGroupShape_Shape); + } + */ + + //remove and delete series-group-shape if empty + + //... todo + + SAL_INFO( + "chart2", + "skipped points: " << nSkippedPoints << " created points: " + << nCreatedPoints); +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/AreaChart.hxx b/chart2/source/view/charttypes/AreaChart.hxx new file mode 100644 index 000000000..2f7434f3c --- /dev/null +++ b/chart2/source/view/charttypes/AreaChart.hxx @@ -0,0 +1,86 @@ +/* -*- 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 . + */ + +#pragma once + +#include <memory> +#include <VSeriesPlotter.hxx> +#include <com/sun/star/chart2/CurveStyle.hpp> + +namespace chart +{ + +class AreaChart : public VSeriesPlotter +{ + // public methods +public: + AreaChart() = delete; + + AreaChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bCategoryXAxis, bool bNoArea=false + ); + virtual ~AreaChart() override; + + virtual void createShapes() override; + virtual void addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + + // MinimumAndMaximumSupplier + virtual bool isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) override; + + virtual LegendSymbolStyle getLegendSymbolStyle() override; + virtual css::uno::Any getExplicitSymbol( const VDataSeries& rSeries, sal_Int32 nPointIndex/*-1 for series symbol*/ ) override; + +private: //methods + void impl_createSeriesShapes(); + bool impl_createArea( VDataSeries* pSeries + , std::vector<std::vector<css::drawing::Position3D>> const * pSeriesPoly + , std::vector<std::vector<css::drawing::Position3D>> const * pPreviousSeriesPoly + , PlottingPositionHelper const * pPosHelper ); + bool impl_createLine( VDataSeries* pSeries + , std::vector<std::vector<css::drawing::Position3D>> const * pSeriesPoly + , PlottingPositionHelper* pPosHelper ); + static bool create_stepped_line( std::vector<std::vector<css::drawing::Position3D>> aStartPoly + , css::chart2::CurveStyle eCurveStyle + , PlottingPositionHelper const * pPosHelper + , std::vector<std::vector<css::drawing::Position3D>> &aPoly ); + +private: //member + std::unique_ptr<PlottingPositionHelper> + m_pMainPosHelper; + + bool m_bArea;//false -> line or symbol only + bool m_bLine; + bool m_bSymbol; + + //Properties for splines: + css::chart2::CurveStyle m_eCurveStyle; + sal_Int32 m_nCurveResolution; + sal_Int32 m_nSplineOrder; + + rtl::Reference<SvxShapeGroupAnyD> m_xSeriesTarget; + rtl::Reference<SvxShapeGroupAnyD> m_xErrorBarTarget; + rtl::Reference<SvxShapeGroupAnyD> m_xTextTarget; + rtl::Reference<SvxShapeGroupAnyD> m_xRegressionCurveEquationTarget; +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BarChart.cxx b/chart2/source/view/charttypes/BarChart.cxx new file mode 100644 index 000000000..b15706d76 --- /dev/null +++ b/chart2/source/view/charttypes/BarChart.cxx @@ -0,0 +1,976 @@ +/* -*- 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 "BarChart.hxx" +#include "BarPositionHelper.hxx" + +#include <ChartType.hxx> +#include <ShapeFactory.hxx> +#include <CommonConverters.hxx> +#include <ObjectIdentifier.hxx> +#include <LabelPositionHelper.hxx> +#include <AxisIndexDefines.hxx> +#include <Clipping.hxx> +#include <DateHelper.hxx> +#include <svx/scene3d.hxx> +#include <comphelper/scopeguard.hxx> + +#include <com/sun/star/chart/DataLabelPlacement.hpp> + +#include <com/sun/star/chart2/DataPointGeometry3D.hpp> +#include <rtl/math.hxx> +#include <tools/diagnose_ex.h> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::rtl::math; +using namespace ::com::sun::star::chart2; + +BarChart::BarChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount ) + , m_pMainPosHelper( new BarPositionHelper() ) +{ + PlotterBase::m_pPosHelper = m_pMainPosHelper.get(); + VSeriesPlotter::m_pMainPosHelper = m_pMainPosHelper.get(); + + try + { + if( m_xChartTypeModel.is() ) + { + m_xChartTypeModel->getPropertyValue( "OverlapSequence" ) >>= m_aOverlapSequence; + m_xChartTypeModel->getPropertyValue( "GapwidthSequence" ) >>= m_aGapwidthSequence; + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } +} + +BarChart::~BarChart() +{ +} + +PlottingPositionHelper& BarChart::getPlottingPositionHelper( sal_Int32 nAxisIndex ) const +{ + PlottingPositionHelper& rPosHelper = VSeriesPlotter::getPlottingPositionHelper( nAxisIndex ); + BarPositionHelper* pBarPosHelper = dynamic_cast<BarPositionHelper*>(&rPosHelper); + if( pBarPosHelper && nAxisIndex >= 0 ) + { + if( nAxisIndex < m_aOverlapSequence.getLength() ) + pBarPosHelper->setInnerDistance( -m_aOverlapSequence[nAxisIndex]/100.0 ); + if( nAxisIndex < m_aGapwidthSequence.getLength() ) + pBarPosHelper->setOuterDistance( m_aGapwidthSequence[nAxisIndex]/100.0 ); + } + return rPosHelper; +} + +drawing::Direction3D BarChart::getPreferredDiagramAspectRatio() const +{ + drawing::Direction3D aRet(1.0,1.0,1.0); + if( m_nDimension == 3 ) + { + aRet = drawing::Direction3D(1.0,-1.0,1.0); + BarPositionHelper* pPosHelper = dynamic_cast<BarPositionHelper*>(&( getPlottingPositionHelper( MAIN_AXIS_INDEX) ) ); + if (pPosHelper) + { + drawing::Direction3D aScale( pPosHelper->getScaledLogicWidth() ); + if(aScale.DirectionX!=0.0) + { + double fXSlotCount = 1.0; + if(!m_aZSlots.empty()) + { + fXSlotCount = m_aZSlots.begin()->size(); + } + aRet.DirectionZ = aScale.DirectionZ / + (aScale.DirectionX + aScale.DirectionX * (fXSlotCount-1.0) * pPosHelper->getScaledSlotWidth()); + } + else + { + return VSeriesPlotter::getPreferredDiagramAspectRatio(); + } + } + else + { + return VSeriesPlotter::getPreferredDiagramAspectRatio(); + } + + if(aRet.DirectionZ<0.05) + { + aRet.DirectionZ=0.05; + } + else if(aRet.DirectionZ>10) + { + aRet.DirectionZ=10; + } + if( m_pMainPosHelper && m_pMainPosHelper->isSwapXAndY() ) + { + double fTemp = aRet.DirectionX; + aRet.DirectionX = aRet.DirectionY; + aRet.DirectionY = fTemp; + } + } + else + aRet = drawing::Direction3D(-1,-1,-1); + return aRet; +} + +awt::Point BarChart::getLabelScreenPositionAndAlignment( + LabelAlignment& rAlignment, sal_Int32 nLabelPlacement + , double fScaledX, double fScaledLowerYValue, double fScaledUpperYValue, double fScaledZ + , double fScaledLowerBarDepth, double fScaledUpperBarDepth, double fBaseValue + , BarPositionHelper const * pPosHelper + ) const +{ + double fX = fScaledX; + double fY = fScaledUpperYValue; + double fZ = fScaledZ; + bool bReverse = !pPosHelper->isMathematicalOrientationY(); + bool bNormalOutside = (!bReverse == (fBaseValue < fScaledUpperYValue)); + double fDepth = fScaledUpperBarDepth; + + switch(nLabelPlacement) + { + case css::chart::DataLabelPlacement::TOP: + { + if( !pPosHelper->isSwapXAndY() ) + { + fY = bReverse ? fScaledLowerYValue : fScaledUpperYValue; + rAlignment = LABEL_ALIGN_TOP; + if(m_nDimension==3) + fDepth = bReverse ? fabs(fScaledLowerBarDepth) : fabs(fScaledUpperBarDepth); + } + else + { + fY -= (fScaledUpperYValue-fScaledLowerYValue)/2.0; + rAlignment = LABEL_ALIGN_CENTER; + OSL_FAIL( "top label placement is not really supported by horizontal bar charts" ); + } + } + break; + case css::chart::DataLabelPlacement::BOTTOM: + { + if(!pPosHelper->isSwapXAndY()) + { + fY = bReverse ? fScaledUpperYValue : fScaledLowerYValue; + rAlignment = LABEL_ALIGN_BOTTOM; + if(m_nDimension==3) + fDepth = bReverse ? fabs(fScaledUpperBarDepth) : fabs(fScaledLowerBarDepth); + } + else + { + fY -= (fScaledUpperYValue-fScaledLowerYValue)/2.0; + rAlignment = LABEL_ALIGN_CENTER; + OSL_FAIL( "bottom label placement is not supported by horizontal bar charts" ); + } + } + break; + case css::chart::DataLabelPlacement::LEFT: + { + if( pPosHelper->isSwapXAndY() ) + { + fY = bReverse ? fScaledUpperYValue : fScaledLowerYValue; + rAlignment = LABEL_ALIGN_LEFT; + if(m_nDimension==3) + fDepth = bReverse ? fabs(fScaledUpperBarDepth) : fabs(fScaledLowerBarDepth); + } + else + { + fY -= (fScaledUpperYValue-fScaledLowerYValue)/2.0; + rAlignment = LABEL_ALIGN_CENTER; + OSL_FAIL( "left label placement is not supported by column charts" ); + } + } + break; + case css::chart::DataLabelPlacement::RIGHT: + { + if( pPosHelper->isSwapXAndY() ) + { + fY = bReverse ? fScaledLowerYValue : fScaledUpperYValue; + rAlignment = LABEL_ALIGN_RIGHT; + if(m_nDimension==3) + fDepth = bReverse ? fabs(fScaledLowerBarDepth) : fabs(fScaledUpperBarDepth); + } + else + { + fY -= (fScaledUpperYValue-fScaledLowerYValue)/2.0; + rAlignment = LABEL_ALIGN_CENTER; + OSL_FAIL( "right label placement is not supported by column charts" ); + } + } + break; + case css::chart::DataLabelPlacement::OUTSIDE: + { + fY = (fBaseValue < fScaledUpperYValue) ? fScaledUpperYValue : fScaledLowerYValue; + if( pPosHelper->isSwapXAndY() ) + // if datapoint value is 0 the label will appear RIGHT in case of Bar Chart + if( fBaseValue == fScaledUpperYValue && fBaseValue == fScaledLowerYValue ) + rAlignment = LABEL_ALIGN_RIGHT; + else + rAlignment = bNormalOutside ? LABEL_ALIGN_RIGHT : LABEL_ALIGN_LEFT; + else + // if datapoint value is 0 the label will appear TOP in case of Column Chart + if( fBaseValue == fScaledUpperYValue && fBaseValue == fScaledLowerYValue ) + rAlignment = LABEL_ALIGN_TOP; + else + rAlignment = bNormalOutside ? LABEL_ALIGN_TOP : LABEL_ALIGN_BOTTOM; + if(m_nDimension==3) + fDepth = (fBaseValue < fScaledUpperYValue) ? fabs(fScaledUpperBarDepth) : fabs(fScaledLowerBarDepth); + } + break; + case css::chart::DataLabelPlacement::INSIDE: + { + fY = (fBaseValue < fScaledUpperYValue) ? fScaledUpperYValue : fScaledLowerYValue; + if( pPosHelper->isSwapXAndY() ) + rAlignment = bNormalOutside ? LABEL_ALIGN_LEFT : LABEL_ALIGN_RIGHT; + else + rAlignment = bNormalOutside ? LABEL_ALIGN_BOTTOM : LABEL_ALIGN_TOP; + if(m_nDimension==3) + fDepth = (fBaseValue < fScaledUpperYValue) ? fabs(fScaledUpperBarDepth) : fabs(fScaledLowerBarDepth); + } + break; + case css::chart::DataLabelPlacement::NEAR_ORIGIN: + { + fY = (fBaseValue < fScaledUpperYValue) ? fScaledLowerYValue : fScaledUpperYValue; + if( pPosHelper->isSwapXAndY() ) + // if datapoint value is 0 the label will appear RIGHT in case of Bar Chart + if( fBaseValue == fScaledUpperYValue && fBaseValue == fScaledLowerYValue ) + rAlignment = LABEL_ALIGN_RIGHT; + else + rAlignment = bNormalOutside ? LABEL_ALIGN_RIGHT : LABEL_ALIGN_LEFT; + else + // if datapoint value is 0 the label will appear TOP in case of Column Chart + if( fBaseValue == fScaledUpperYValue && fBaseValue == fScaledLowerYValue ) + rAlignment = LABEL_ALIGN_TOP; + else + rAlignment = bNormalOutside ? LABEL_ALIGN_TOP : LABEL_ALIGN_BOTTOM; + if(m_nDimension==3) + fDepth = (fBaseValue < fScaledUpperYValue) ? fabs(fScaledLowerBarDepth) : fabs(fScaledUpperBarDepth); + } + break; + case css::chart::DataLabelPlacement::CENTER: + fY -= (fScaledUpperYValue-fScaledLowerYValue)/2.0; + // if datapoint value is 0 the label will appear TOP/RIGHT in case of Column/Bar Charts + if( fBaseValue == fScaledUpperYValue && fBaseValue == fScaledLowerYValue ) + if( pPosHelper->isSwapXAndY() ) + rAlignment = LABEL_ALIGN_RIGHT; + else + rAlignment = LABEL_ALIGN_TOP; + else + rAlignment = LABEL_ALIGN_CENTER; + if(m_nDimension==3) + fDepth = fabs(fScaledUpperBarDepth-fScaledLowerBarDepth)/2.0; + break; + default: + OSL_FAIL("this label alignment is not implemented yet"); + + break; + } + if(m_nDimension==3) + fZ -= fDepth/2.0; + + drawing::Position3D aScenePosition3D( pPosHelper-> + transformScaledLogicToScene( fX, fY, fZ, true ) ); + return LabelPositionHelper(m_nDimension,m_xLogicTarget) + .transformSceneToScreenPosition( aScenePosition3D ); +} + +rtl::Reference< SvxShape > BarChart::createDataPoint3D_Bar( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , const drawing::Position3D& rPosition, const drawing::Direction3D& rSize + , double fTopHeight, sal_Int32 nRotateZAngleHundredthDegree + , const uno::Reference< beans::XPropertySet >& xObjectProperties + , sal_Int32 nGeometry3D ) +{ + bool bRoundedEdges = true; + try + { + if( xObjectProperties.is() ) + { + sal_Int16 nPercentDiagonal = 0; + xObjectProperties->getPropertyValue( "PercentDiagonal" ) >>= nPercentDiagonal; + if( nPercentDiagonal < 5 ) + bRoundedEdges = false; + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + rtl::Reference< SvxShape > xShape; + switch( nGeometry3D ) + { + case DataPointGeometry3D::CYLINDER: + xShape = ShapeFactory::createCylinder( xTarget, rPosition, rSize, nRotateZAngleHundredthDegree ); + break; + case DataPointGeometry3D::CONE: + xShape = ShapeFactory::createCone( xTarget, rPosition, rSize, fTopHeight, nRotateZAngleHundredthDegree ); + break; + case DataPointGeometry3D::PYRAMID: + xShape = ShapeFactory::createPyramid( xTarget, rPosition, rSize, fTopHeight, nRotateZAngleHundredthDegree>0 + , xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + break; + case DataPointGeometry3D::CUBOID: + default: + xShape = ShapeFactory::createCube( xTarget, rPosition, rSize + , nRotateZAngleHundredthDegree, xObjectProperties + , PropertyMapper::getPropertyNameMapForFilledSeriesProperties(), bRoundedEdges ); + return xShape; + } + if( nGeometry3D != DataPointGeometry3D::PYRAMID ) + PropertyMapper::setMappedProperties( *xShape, xObjectProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + return xShape; +} + +namespace +{ +bool lcl_hasGeometry3DVariableWidth( sal_Int32 nGeometry3D ) +{ + bool bRet = false; + switch( nGeometry3D ) + { + case DataPointGeometry3D::PYRAMID: + case DataPointGeometry3D::CONE: + bRet = true; + break; + case DataPointGeometry3D::CUBOID: + case DataPointGeometry3D::CYLINDER: + default: + bRet = false; + break; + } + return bRet; +} +}// end anonymous namespace + +void BarChart::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) +{ + if( !pSeries ) + return; + if(m_nDimension==2) + { + //2ND_AXIS_IN_BARS put series on second scales to different z slot as temporary workaround + //this needs to be redesigned if 3d bars are also able to display secondary axes + + sal_Int32 nAxisIndex = pSeries->getAttachedAxisIndex(); + zSlot = nAxisIndex; + + if( !pSeries->getGroupBarsPerAxis() ) + zSlot = 0; + if(zSlot>=static_cast<sal_Int32>(m_aZSlots.size())) + m_aZSlots.resize(zSlot+1); + } + VSeriesPlotter::addSeries( std::move(pSeries), zSlot, xSlot, ySlot ); +} + +void BarChart::adaptOverlapAndGapwidthForGroupBarsPerAxis() +{ + //adapt m_aOverlapSequence and m_aGapwidthSequence for the groupBarsPerAxis feature + //thus the different series use the same settings + + VDataSeries* pFirstSeries = getFirstSeries(); + if(!pFirstSeries || pFirstSeries->getGroupBarsPerAxis()) + return; + + sal_Int32 nAxisIndex = pFirstSeries->getAttachedAxisIndex(); + sal_Int32 nN = 0; + sal_Int32 nUseThisIndex = nAxisIndex; + if( nUseThisIndex < 0 || nUseThisIndex >= m_aOverlapSequence.getLength() ) + nUseThisIndex = 0; + auto aOverlapSequenceRange = asNonConstRange(m_aOverlapSequence); + for( nN = 0; nN < m_aOverlapSequence.getLength(); nN++ ) + { + if(nN!=nUseThisIndex) + aOverlapSequenceRange[nN] = m_aOverlapSequence[nUseThisIndex]; + } + + nUseThisIndex = nAxisIndex; + if( nUseThisIndex < 0 || nUseThisIndex >= m_aGapwidthSequence.getLength() ) + nUseThisIndex = 0; + auto aGapwidthSequenceRange = asNonConstRange(m_aGapwidthSequence); + for( nN = 0; nN < m_aGapwidthSequence.getLength(); nN++ ) + { + if(nN!=nUseThisIndex) + aGapwidthSequenceRange[nN] = m_aGapwidthSequence[nUseThisIndex]; + } +} + +void BarChart::createShapes() +{ + if( m_aZSlots.empty() ) //no series + return; + + OSL_ENSURE(m_xLogicTarget.is()&&m_xFinalTarget.is(),"BarChart is not proper 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) + + //the regression curves should always be on top of the bars but beneath the text labels + //to achieve this the regression curve target is created after the series target and before the text target + + rtl::Reference<SvxShapeGroupAnyD> xSeriesTarget = createGroupShape( m_xLogicTarget ); + rtl::Reference<SvxShapeGroupAnyD> xRegressionCurveTarget = createGroupShape( m_xLogicTarget ); + rtl::Reference<SvxShapeGroupAnyD> xTextTarget = ShapeFactory::createGroup2D( m_xFinalTarget ); + + rtl::Reference<SvxShapeGroupAnyD> xRegressionCurveEquationTarget = + ShapeFactory::createGroup2D( m_xFinalTarget ); + //check necessary here that different Y axis can not be stacked in the same group? ... hm? + + bool bDrawConnectionLines = false; + bool bDrawConnectionLinesInited = false; + + std::unordered_set<rtl::Reference<SvxShape>> aShapeSet; + + const comphelper::ScopeGuard aGuard([aShapeSet]() { + + std::unordered_set<E3dScene*> aSceneSet; + + for (rtl::Reference<SvxShape> const & rShape : aShapeSet) + { + E3dScene* pScene = dynamic_cast<E3dScene*>(rShape->GetSdrObject()); + if(nullptr != pScene) + { + aSceneSet.insert(pScene->getRootE3dSceneFromE3dObject()); + } + } + for (E3dScene* pScene : aSceneSet) + { + pScene->ResumeReportingDirtyRects(); + pScene->SetAllSceneRectsDirty(); + } + }); + + adaptOverlapAndGapwidthForGroupBarsPerAxis(); + + //better performance for big data + std::map< VDataSeries*, FormerBarPoint > aSeriesFormerPointMap; + m_bPointsWereSkipped = false; + + sal_Int32 nStartIndex = 0; + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + //iterate through all x values per indices + for( sal_Int32 nPointIndex = nStartIndex; nPointIndex < nEndIndex; nPointIndex++ ) + { + //sum up the values for all series in a complete z slot per attached axis + std::map< sal_Int32, double > aLogicYSumMap; + for( auto& rZSlot : m_aZSlots ) + { + for( auto& rXSlot : rZSlot ) + { + sal_Int32 nAttachedAxisIndex = rXSlot.getAttachedAxisIndexForFirstSeries(); + aLogicYSumMap.insert({nAttachedAxisIndex, 0.0}); + + const sal_Int32 nSlotPoints = rXSlot.getPointCount(); + if( nPointIndex >= nSlotPoints ) + continue; + + double fMinimumY = 0.0, fMaximumY = 0.0; + rXSlot.calculateYMinAndMaxForCategory( nPointIndex + , isSeparateStackingForDifferentSigns( 1 ), fMinimumY, fMaximumY, nAttachedAxisIndex ); + + if( !std::isnan( fMaximumY ) && fMaximumY > 0) + aLogicYSumMap[nAttachedAxisIndex] += fMaximumY; + if( !std::isnan( fMinimumY ) && fMinimumY < 0) + aLogicYSumMap[nAttachedAxisIndex] += fabs(fMinimumY); + } + } + + sal_Int32 nZ=1; + for( auto& rZSlot : m_aZSlots ) + { + doZSlot(bDrawConnectionLines, bDrawConnectionLinesInited, rZSlot, nZ, nPointIndex, nStartIndex, + xSeriesTarget, xRegressionCurveTarget, xRegressionCurveEquationTarget, xTextTarget, + aShapeSet, aSeriesFormerPointMap, aLogicYSumMap); + ++nZ; + }//next z slot + }//next category + if( bDrawConnectionLines ) + { + for( auto const& rZSlot : m_aZSlots ) + { + BarPositionHelper* pPosHelper = m_pMainPosHelper.get(); + if( !rZSlot.empty() ) + { + sal_Int32 nAttachedAxisIndex = rZSlot.front().getAttachedAxisIndexForFirstSeries(); + //2ND_AXIS_IN_BARS so far one can assume to have the same plotter for each z slot + pPosHelper = dynamic_cast<BarPositionHelper*>(&( getPlottingPositionHelper( nAttachedAxisIndex ) ) ); + if(!pPosHelper) + pPosHelper = m_pMainPosHelper.get(); + } + PlotterBase::m_pPosHelper = pPosHelper; + + //iterate through all x slots in this category + for( auto const& rXSlot : rZSlot ) + { + //iterate through all series in this x slot + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly = &pSeries->m_aPolyPolygonShape3D; + if(!ShapeFactory::hasPolygonAnyLines(*pSeriesPoly)) + continue; + + std::vector<std::vector<css::drawing::Position3D>> aPoly; + Clipping::clipPolygonAtRectangle( *pSeriesPoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + + if(!ShapeFactory::hasPolygonAnyLines(aPoly)) + continue; + + //transformation 3) -> 4) + pPosHelper->transformScaledLogicToScene( aPoly ); + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes( + getSeriesGroupShape(pSeries.get(), xSeriesTarget) ); + rtl::Reference<SvxShapePolyPolygon> xShape( ShapeFactory::createLine2D( + xSeriesGroupShape_Shapes, aPoly ) ); + PropertyMapper::setMappedProperties( *xShape, pSeries->getPropertiesOfSeries() + , PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + } + } + } + } + + /* @todo remove series shapes if empty + */ +} + +void BarChart::doZSlot( + bool& bDrawConnectionLines, bool& bDrawConnectionLinesInited, + const std::vector< VDataSeriesGroup >& rZSlot, + const sal_Int32 nZ, const sal_Int32 nPointIndex, const sal_Int32 nStartIndex, + const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveEquationTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + std::unordered_set<rtl::Reference<SvxShape>>& aShapeSet, + std::map< VDataSeries*, FormerBarPoint >& aSeriesFormerPointMap, + std::map< sal_Int32, double >& aLogicYSumMap) +{ + //iterate through all x slots in this category + double fSlotX=0; + for( auto& rXSlot : rZSlot ) + { + sal_Int32 nAttachedAxisIndex = rXSlot.getAttachedAxisIndexForFirstSeries(); + //2ND_AXIS_IN_BARS so far one can assume to have the same plotter for each z slot + BarPositionHelper* pPosHelper = dynamic_cast<BarPositionHelper*>(&( getPlottingPositionHelper( nAttachedAxisIndex ) ) ); + if(!pPosHelper) + pPosHelper = m_pMainPosHelper.get(); + + PlotterBase::m_pPosHelper = pPosHelper; + + //update/create information for current group + pPosHelper->updateSeriesCount( rZSlot.size() ); + double fLogicBaseWidth = pPosHelper->getScaledSlotWidth(); + + // get distance from base value to maximum and minimum + + double fMinimumY = 0.0, fMaximumY = 0.0; + if( nPointIndex < rXSlot.getPointCount()) + rXSlot.calculateYMinAndMaxForCategory( nPointIndex + , isSeparateStackingForDifferentSigns( 1 ), fMinimumY, fMaximumY, nAttachedAxisIndex ); + + double fLogicPositiveYSum = 0.0; + if( !std::isnan( fMaximumY ) ) + fLogicPositiveYSum = fMaximumY; + + double fLogicNegativeYSum = 0.0; + if( !std::isnan( fMinimumY ) ) + fLogicNegativeYSum = fMinimumY; + + if( pPosHelper->isPercentY() ) + { + /* #i70395# fLogicPositiveYSum contains sum of all positive + values, if any, otherwise the highest negative value. + fLogicNegativeYSum contains sum of all negative values, + if any, otherwise the lowest positive value. + Afterwards, fLogicPositiveYSum will contain the maximum + (positive) value that is related to 100%. */ + + // do nothing if there are positive values only + if( fLogicNegativeYSum < 0.0 ) + { + // fLogicPositiveYSum<0 => negative values only, use absolute of negative sum + if( fLogicPositiveYSum < 0.0 ) + fLogicPositiveYSum = -fLogicNegativeYSum; + // otherwise there are positive and negative values, calculate total distance + else + fLogicPositiveYSum -= fLogicNegativeYSum; + } + fLogicNegativeYSum = 0.0; + } + + doXSlot(rXSlot, bDrawConnectionLines, bDrawConnectionLinesInited, nZ, nPointIndex, nStartIndex, + xSeriesTarget, xRegressionCurveTarget, xRegressionCurveEquationTarget, xTextTarget, + aShapeSet, aSeriesFormerPointMap, aLogicYSumMap, + fLogicBaseWidth, fSlotX, pPosHelper, fLogicPositiveYSum, fLogicNegativeYSum, nAttachedAxisIndex); + + fSlotX+=1.0; + }//next x slot +} + + +void BarChart::doXSlot( + const VDataSeriesGroup& rXSlot, + bool& bDrawConnectionLines, bool& bDrawConnectionLinesInited, + const sal_Int32 nZ, const sal_Int32 nPointIndex, const sal_Int32 nStartIndex, + const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveEquationTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + std::unordered_set<rtl::Reference<SvxShape>>& aShapeSet, + std::map< VDataSeries*, FormerBarPoint >& aSeriesFormerPointMap, + std::map< sal_Int32, double >& aLogicYSumMap, + const double fLogicBaseWidth, const double fSlotX, + BarPositionHelper* const pPosHelper, + const double fLogicPositiveYSum, const double fLogicNegativeYSum, + const sal_Int32 nAttachedAxisIndex) +{ + double fBaseValue = 0.0; + if( !pPosHelper->isPercentY() && rXSlot.m_aSeriesVector.size()<=1 ) + fBaseValue = pPosHelper->getBaseValueY(); + double fPositiveLogicYForNextSeries = fBaseValue; + double fNegativeLogicYForNextSeries = fBaseValue; + + //iterate through all series in this x slot + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor"); + + bool bOnlyConnectionLinesForThisPoint = false; + + if(nPointIndex==nStartIndex)//do not create a regression line for each point + createRegressionCurvesShapes( *pSeries, xRegressionCurveTarget, xRegressionCurveEquationTarget, + m_pPosHelper->maySkipPointsInRegressionCalculation()); + + if( !bDrawConnectionLinesInited ) + { + bDrawConnectionLines = pSeries->getConnectBars(); + if( m_nDimension==3 ) + bDrawConnectionLines = false; + if( bDrawConnectionLines && rXSlot.m_aSeriesVector.size()==1 ) + { + //detect whether we have a stacked chart or not: + StackingDirection eDirection = pSeries->getStackingDirection(); + if( eDirection != StackingDirection_Y_STACKING ) + bDrawConnectionLines = false; + } + bDrawConnectionLinesInited = true; + } + + // Use another XShapes for background, so we can avoid needing to set the Z-order on all of them, + // which is expensive in bulk. + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes(getSeriesGroupShape(pSeries.get(), xSeriesTarget)); + rtl::Reference<SvxShapeGroupAnyD> xSeriesBackgroundShape_Shapes(getSeriesGroupShape(pSeries.get(), xSeriesTarget)); + aShapeSet.insert(xSeriesGroupShape_Shapes); + aShapeSet.insert(xSeriesBackgroundShape_Shapes); + // Suspend setting rects dirty for the duration of this call + E3dScene* pScene = dynamic_cast<E3dScene*>(xSeriesGroupShape_Shapes->GetSdrObject()); + if (pScene) + pScene->SuspendReportingDirtyRects(); + pScene = dynamic_cast<E3dScene*>(xSeriesBackgroundShape_Shapes->GetSdrObject()); + if (pScene) + pScene->SuspendReportingDirtyRects(); + + //collect data point information (logic coordinates, style ): + double fUnscaledLogicX = pSeries->getXValue( nPointIndex ); + fUnscaledLogicX = DateHelper::RasterizeDateValue( fUnscaledLogicX, m_aNullDate, m_nTimeResolution ); + if(std::isnan(fUnscaledLogicX)) + continue;//point not visible + if(fUnscaledLogicX<pPosHelper->getLogicMinX()) + continue;//point not visible + if(fUnscaledLogicX>pPosHelper->getLogicMaxX()) + continue;//point not visible + if(pPosHelper->isStrongLowerRequested(0) && fUnscaledLogicX==pPosHelper->getLogicMaxX()) + continue;//point not visible + double fLogicX = pPosHelper->getScaledSlotPos( fUnscaledLogicX, fSlotX ); + + double fLogicBarHeight = pSeries->getYValue( nPointIndex ); + if( std::isnan( fLogicBarHeight )) //no value at this category + continue; + + double fLogicValueForLabeDisplay = fLogicBarHeight; + fLogicBarHeight-=fBaseValue; + + if( pPosHelper->isPercentY() ) + { + if(fLogicPositiveYSum!=0.0) + fLogicBarHeight = fabs( fLogicBarHeight )/fLogicPositiveYSum; + else + fLogicBarHeight = 0.0; + } + + // tdf#114141 to draw the top of the zero height 3D bar + // we set a small positive value, here the smallest one for the type double (DBL_MIN) + if( fLogicBarHeight == 0.0 ) + fLogicBarHeight = DBL_MIN; + + //sort negative and positive values, to display them on different sides of the x axis + bool bPositive = fLogicBarHeight >= 0.0; + double fLowerYValue = bPositive ? fPositiveLogicYForNextSeries : fNegativeLogicYForNextSeries; + double fUpperYValue = fLowerYValue+fLogicBarHeight; + if( bPositive ) + fPositiveLogicYForNextSeries += fLogicBarHeight; + else + fNegativeLogicYForNextSeries += fLogicBarHeight; + + double fLogicZ = 1.0;//as defined + if(m_nDimension==3) + fLogicZ = nZ+0.5; + + drawing::Position3D aUnscaledLogicPosition( fUnscaledLogicX, fUpperYValue, fLogicZ ); + + //@todo ... start an iteration over the different breaks of the axis + //each subsystem may add an additional shape to form the whole point + //create a group shape for this point and add to the series shape: +// uno::Reference< drawing::XShapes > xPointGroupShape_Shapes( createGroupShape(xSeriesGroupShape_Shapes) ); +// uno::Reference<drawing::XShape> xPointGroupShape_Shape = +// uno::Reference<drawing::XShape>( xPointGroupShape_Shapes, uno::UNO_QUERY ); + //as long as we do not iterate we do not need to create an additional group for each point + uno::Reference< beans::XPropertySet > xDataPointProperties( pSeries->getPropertiesOfPoint( nPointIndex ) ); + sal_Int32 nGeometry3D = DataPointGeometry3D::CUBOID; + if(m_nDimension==3) try + { + xDataPointProperties->getPropertyValue( "Geometry3D") >>= nGeometry3D; + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + //@todo iterate through all subsystems to create partial points + { + //@todo select a suitable PositionHelper for this subsystem + BarPositionHelper* pSubPosHelper = pPosHelper; + + double fUnclippedUpperYValue = fUpperYValue; + + //apply clipping to Y + if( !pPosHelper->clipYRange(fLowerYValue,fUpperYValue) ) + { + if( bDrawConnectionLines ) + bOnlyConnectionLinesForThisPoint = true; + else + continue; + } + //@todo clipping of X and Z is not fully integrated so far, as there is a need to create different objects + + //apply scaling to Y before calculating width (necessary to maintain gradient in clipped objects) + pSubPosHelper->doLogicScaling(nullptr,&fLowerYValue,nullptr); + pSubPosHelper->doLogicScaling(nullptr,&fUpperYValue,nullptr); + //scaling of X and Z is not provided as the created objects should be symmetric in that dimensions + + pSubPosHelper->doLogicScaling(nullptr,&fUnclippedUpperYValue,nullptr); + + //calculate resulting width + double fCompleteHeight = bPositive ? fLogicPositiveYSum : fLogicNegativeYSum; + if( pPosHelper->isPercentY() ) + fCompleteHeight = 1.0; + double fLogicBarWidth = fLogicBaseWidth; + double fTopHeight=approxSub(fCompleteHeight,fUpperYValue); + if(!bPositive) + fTopHeight=approxSub(fCompleteHeight,fLowerYValue); + double fLogicYStart = bPositive ? fLowerYValue : fUpperYValue; + double fMiddleHeight = fUpperYValue-fLowerYValue; + if(!bPositive) + fMiddleHeight*=-1.0; + double fLogicBarDepth = 0.5; + if(m_nDimension==3) + { + if( lcl_hasGeometry3DVariableWidth(nGeometry3D) && fCompleteHeight!=0.0 ) + { + double fHeight = fCompleteHeight-fLowerYValue; + if(!bPositive) + fHeight = fCompleteHeight-fUpperYValue; + fLogicBarWidth = fLogicBaseWidth*fHeight/fCompleteHeight; + if(fLogicBarWidth<=0.0) + fLogicBarWidth=fLogicBaseWidth; + fLogicBarDepth = fLogicBarDepth*fHeight/fCompleteHeight; + if(fLogicBarDepth<=0.0) + fLogicBarDepth*=-1.0; + } + } + + //better performance for big data + FormerBarPoint aFormerPoint( aSeriesFormerPointMap[pSeries.get()] ); + pPosHelper->setCoordinateSystemResolution( m_aCoordinateSystemResolution ); + if( !pSeries->isAttributedDataPoint(nPointIndex) + && + pPosHelper->isSameForGivenResolution( aFormerPoint.m_fX, aFormerPoint.m_fUpperY, aFormerPoint.m_fZ + , fLogicX, fUpperYValue, fLogicZ ) + && + pPosHelper->isSameForGivenResolution( aFormerPoint.m_fX, aFormerPoint.m_fLowerY, aFormerPoint.m_fZ + , fLogicX, fLowerYValue, fLogicZ ) + ) + { + m_bPointsWereSkipped = true; + continue; + } + aSeriesFormerPointMap[pSeries.get()] = FormerBarPoint(fLogicX,fUpperYValue,fLowerYValue,fLogicZ); + + if( bDrawConnectionLines ) + { + //store point information for connection lines + + drawing::Position3D aLeftUpperPoint( fLogicX-fLogicBarWidth/2.0,fUnclippedUpperYValue,fLogicZ ); + drawing::Position3D aRightUpperPoint( fLogicX+fLogicBarWidth/2.0,fUnclippedUpperYValue,fLogicZ ); + + if( isValidPosition(aLeftUpperPoint) ) + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aLeftUpperPoint ); + if( isValidPosition(aRightUpperPoint) ) + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aRightUpperPoint ); + } + + if( bOnlyConnectionLinesForThisPoint ) + continue; + + //maybe additional possibility for performance improvement + //bool bCreateLineInsteadOfComplexGeometryDueToMissingSpace = false; + //pPosHelper->isSameForGivenResolution( fLogicX-fLogicBarWidth/2.0, fLowerYValue, fLogicZ + // , fLogicX+fLogicBarWidth/2.0, fLowerYValue, fLogicZ ); + + //create partial point + if( !approxEqual(fLowerYValue,fUpperYValue) ) + { + rtl::Reference< SvxShape > xShape; + if( m_nDimension==3 ) + { + drawing::Position3D aLogicBottom (fLogicX,fLogicYStart,fLogicZ); + drawing::Position3D aLogicLeftBottomFront (fLogicX+fLogicBarWidth/2.0,fLogicYStart,fLogicZ-fLogicBarDepth/2.0); + drawing::Position3D aLogicRightDeepTop (fLogicX-fLogicBarWidth/2.0,fLogicYStart+fMiddleHeight,fLogicZ+fLogicBarDepth/2.0); + drawing::Position3D aLogicTopTop (fLogicX,fLogicYStart+fMiddleHeight+fTopHeight,fLogicZ); + + ::chart::XTransformation2* pTransformation = pSubPosHelper->getTransformationScaledLogicToScene(); + + //transformation 3) -> 4) + drawing::Position3D aTransformedBottom ( pTransformation->transform( aLogicBottom ) ); + drawing::Position3D aTransformedLeftBottomFront ( pTransformation->transform( aLogicLeftBottomFront ) ); + drawing::Position3D aTransformedRightDeepTop ( pTransformation->transform( aLogicRightDeepTop ) ); + drawing::Position3D aTransformedTopTop ( pTransformation->transform( aLogicTopTop ) ); + + drawing::Direction3D aSize = aTransformedRightDeepTop - aTransformedLeftBottomFront; + drawing::Direction3D aTopSize( aTransformedTopTop - aTransformedRightDeepTop ); + fTopHeight = aTopSize.DirectionY; + + sal_Int32 nRotateZAngleHundredthDegree = 0; + if( pPosHelper->isSwapXAndY() ) + { + fTopHeight = aTopSize.DirectionX; + nRotateZAngleHundredthDegree = 90*100; + aSize = drawing::Direction3D(aSize.DirectionY,aSize.DirectionX,aSize.DirectionZ); + } + + if( aSize.DirectionX < 0 ) + aSize.DirectionX *= -1.0; + if( aSize.DirectionZ < 0 ) + aSize.DirectionZ *= -1.0; + if( fTopHeight < 0 ) + fTopHeight *= -1.0; + + xShape = createDataPoint3D_Bar( + xSeriesGroupShape_Shapes, aTransformedBottom, aSize, fTopHeight, nRotateZAngleHundredthDegree + , xDataPointProperties, nGeometry3D ); + } + else //m_nDimension!=3 + { + drawing::Position3D aLeftUpperPoint( fLogicX-fLogicBarWidth/2.0,fUpperYValue,fLogicZ ); + drawing::Position3D aRightUpperPoint( fLogicX+fLogicBarWidth/2.0,fUpperYValue,fLogicZ ); + std::vector<std::vector<css::drawing::Position3D>> aPoly + { + { // inner vector + drawing::Position3D( fLogicX-fLogicBarWidth/2.0,fLowerYValue,fLogicZ), + drawing::Position3D( fLogicX+fLogicBarWidth/2.0,fLowerYValue,fLogicZ), + aRightUpperPoint, + aLeftUpperPoint, + drawing::Position3D( fLogicX-fLogicBarWidth/2.0,fLowerYValue,fLogicZ) + } + }; + pPosHelper->transformScaledLogicToScene( aPoly ); + xShape = ShapeFactory::createArea2D( xSeriesGroupShape_Shapes, aPoly ); + PropertyMapper::setMappedProperties( *xShape, xDataPointProperties, PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + } + + if(bHasFillColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nPointIndex, "FillColor"); + if(!std::isnan(nPropVal)) + { + xShape->setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>(nPropVal))); + } + } + //set name/classified ObjectID (CID) + ShapeFactory::setShapeName(xShape + , ObjectIdentifier::createPointCID( + pSeries->getPointCID_Stub(),nPointIndex) ); + } + + //create error bar + createErrorBar_Y( aUnscaledLogicPosition, *pSeries, nPointIndex, m_xLogicTarget, &fLogicX ); + + //create data point label + if( pSeries->getDataPointLabelIfLabel(nPointIndex) ) + { + double fLogicSum = aLogicYSumMap[nAttachedAxisIndex]; + + LabelAlignment eAlignment(LABEL_ALIGN_CENTER); + sal_Int32 nLabelPlacement = pSeries->getLabelPlacement( nPointIndex, m_xChartTypeModel, pPosHelper->isSwapXAndY() ); + + double fLowerBarDepth = fLogicBarDepth; + double fUpperBarDepth = fLogicBarDepth; + { + if( lcl_hasGeometry3DVariableWidth(nGeometry3D) && fCompleteHeight!=0.0 ) + { + double fOuterBarDepth = fLogicBarDepth * fTopHeight/(fabs(fCompleteHeight)); + fLowerBarDepth = (fBaseValue < fUpperYValue) ? fabs(fLogicBarDepth) : fabs(fOuterBarDepth); + fUpperBarDepth = (fBaseValue < fUpperYValue) ? fabs(fOuterBarDepth) : fabs(fLogicBarDepth); + } + } + + awt::Point aScreenPosition2D = getLabelScreenPositionAndAlignment( + eAlignment, nLabelPlacement, fLogicX, fLowerYValue, fUpperYValue, fLogicZ, + fLowerBarDepth, fUpperBarDepth, fBaseValue, pPosHelper); + sal_Int32 nOffset = 0; + if(eAlignment!=LABEL_ALIGN_CENTER) + { + nOffset = 100;//add some spacing //@todo maybe get more intelligent values + if( m_nDimension == 3 ) + nOffset = 260; + } + createDataLabel( + xTextTarget, *pSeries, nPointIndex, + fLogicValueForLabeDisplay, fLogicSum, aScreenPosition2D, eAlignment, nOffset); + } + + }//end iteration through partial points + + }//next series in x slot (next y slot) +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BarChart.hxx b/chart2/source/view/charttypes/BarChart.hxx new file mode 100644 index 000000000..52c3b6177 --- /dev/null +++ b/chart2/source/view/charttypes/BarChart.hxx @@ -0,0 +1,118 @@ +/* -*- 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 . + */ + +#pragma once + +#include <memory> +#include <VSeriesPlotter.hxx> + +namespace chart +{ +class BarPositionHelper; + +class BarChart : public VSeriesPlotter +{ + // public methods +public: + BarChart() = delete; + + BarChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount ); + virtual ~BarChart() override; + + virtual void createShapes() override; + virtual void addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + +private: //methods + static rtl::Reference< SvxShape > + createDataPoint3D_Bar( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , const css::drawing::Position3D& rPosition + , const css::drawing::Direction3D& rSize + , double fTopHeight, sal_Int32 nRotateZAngleHundredthDegree + , const css::uno::Reference< css::beans::XPropertySet >& xObjectProperties + , sal_Int32 nGeometry3D ); + + css::awt::Point getLabelScreenPositionAndAlignment( + LabelAlignment& rAlignment, sal_Int32 nLabelPlacement + , double fScaledX, double fScaledLowerYValue, double fScaledUpperYValue, double fScaledZ + , double fScaledLowerBarDepth, double fScaledUpperBarDepth, double fBaseValue + , BarPositionHelper const * pPosHelper ) const; + + virtual PlottingPositionHelper& getPlottingPositionHelper( sal_Int32 nAxisIndex ) const override;//nAxisIndex indicates whether the position belongs to the main axis ( nAxisIndex==0 ) or secondary axis ( nAxisIndex==1 ) + + void adaptOverlapAndGapwidthForGroupBarsPerAxis(); + + //better performance for big data + struct FormerBarPoint + { + FormerBarPoint( double fX, double fUpperY, double fLowerY, double fZ ) + : m_fX(fX), m_fUpperY(fUpperY), m_fLowerY(fLowerY), m_fZ(fZ) + {} + FormerBarPoint() + : m_fX(std::numeric_limits<double>::quiet_NaN()) + , m_fUpperY(std::numeric_limits<double>::quiet_NaN()) + , m_fLowerY(std::numeric_limits<double>::quiet_NaN()) + , m_fZ(std::numeric_limits<double>::quiet_NaN()) + { + } + + double m_fX; + double m_fUpperY; + double m_fLowerY; + double m_fZ; + }; + + void doZSlot( + bool& bDrawConnectionLines, bool& bDrawConnectionLinesInited, const std::vector< VDataSeriesGroup >& rZSlot, + sal_Int32 nZ, sal_Int32 nPointIndex, sal_Int32 nStartIndex, + const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveEquationTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + std::unordered_set<rtl::Reference<SvxShape>>& aShapeSet, + std::map< VDataSeries*, FormerBarPoint >& aSeriesFormerPointMap, + std::map< sal_Int32, double >& aLogicYSumMap); + + void doXSlot( + const VDataSeriesGroup& rXSlot, + bool& bDrawConnectionLines, bool& bDrawConnectionLinesInited, + sal_Int32 nZ, sal_Int32 nPointIndex, sal_Int32 nStartIndex, + const rtl::Reference<SvxShapeGroupAnyD>& xSeriesTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xRegressionCurveEquationTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + std::unordered_set<rtl::Reference<SvxShape>>& aShapeSet, + std::map< VDataSeries*, FormerBarPoint >& aSeriesFormerPointMap, + std::map< sal_Int32, double >& aLogicYSumMap, + double fLogicBaseWidth, double fSlotX, + BarPositionHelper* const pPosHelper, + double fLogicPositiveYSum, double fLogicNegativeYSum, + sal_Int32 nAttachedAxisIndex); + +private: //member + std::unique_ptr<BarPositionHelper> m_pMainPosHelper; + css::uno::Sequence< sal_Int32 > m_aOverlapSequence; + css::uno::Sequence< sal_Int32 > m_aGapwidthSequence; +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BarPositionHelper.cxx b/chart2/source/view/charttypes/BarPositionHelper.cxx new file mode 100644 index 000000000..f8ac3e7d6 --- /dev/null +++ b/chart2/source/view/charttypes/BarPositionHelper.cxx @@ -0,0 +1,73 @@ +/* -*- 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 "BarPositionHelper.hxx" +#include <DateHelper.hxx> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +BarPositionHelper::BarPositionHelper() + : CategoryPositionHelper( 1 ) +{ + AllowShiftXAxisPos(true); + AllowShiftZAxisPos(true); +} + +BarPositionHelper::BarPositionHelper( const BarPositionHelper& rSource ) + : CategoryPositionHelper( rSource ) + , PlottingPositionHelper( rSource ) +{ +} + +BarPositionHelper::~BarPositionHelper() +{ +} + +std::unique_ptr<PlottingPositionHelper> BarPositionHelper::clone() const +{ + return std::make_unique<BarPositionHelper>(*this); +} + +void BarPositionHelper::updateSeriesCount( double fSeriesCount ) +{ + m_fSeriesCount = fSeriesCount; +} + +double BarPositionHelper::getScaledSlotPos( double fUnscaledLogicX, double fSeriesNumber ) const +{ + if( m_bDateAxis ) + fUnscaledLogicX = DateHelper::RasterizeDateValue( fUnscaledLogicX, m_aNullDate, m_nTimeResolution ); + double fScaledLogicX(fUnscaledLogicX); + doLogicScaling(&fScaledLogicX,nullptr,nullptr); + fScaledLogicX = CategoryPositionHelper::getScaledSlotPos( fScaledLogicX, fSeriesNumber ); + return fScaledLogicX; + +} + +void BarPositionHelper::setScaledCategoryWidth( double fScaledCategoryWidth ) +{ + m_fScaledCategoryWidth = fScaledCategoryWidth; + CategoryPositionHelper::setCategoryWidth( m_fScaledCategoryWidth ); +} +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BarPositionHelper.hxx b/chart2/source/view/charttypes/BarPositionHelper.hxx new file mode 100644 index 000000000..961ea988f --- /dev/null +++ b/chart2/source/view/charttypes/BarPositionHelper.hxx @@ -0,0 +1,44 @@ +/* -*- 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 . + */ + +#pragma once + +#include <PlottingPositionHelper.hxx> +#include "CategoryPositionHelper.hxx" + +namespace chart +{ +class BarPositionHelper : public CategoryPositionHelper, public PlottingPositionHelper +{ +public: + explicit BarPositionHelper(); + BarPositionHelper(const BarPositionHelper& rSource); + virtual ~BarPositionHelper() override; + + virtual std::unique_ptr<PlottingPositionHelper> clone() const override; + + void updateSeriesCount(double fSeriesCount); /*only enter the size of x stacked series*/ + + virtual double getScaledSlotPos(double fCategoryX, double fSeriesNumber) const override; + virtual void setScaledCategoryWidth(double fScaledCategoryWidth) override; +}; + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BubbleChart.cxx b/chart2/source/view/charttypes/BubbleChart.cxx new file mode 100644 index 000000000..803cf73b2 --- /dev/null +++ b/chart2/source/view/charttypes/BubbleChart.cxx @@ -0,0 +1,362 @@ +/* -*- 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 "BubbleChart.hxx" +#include <PlottingPositionHelper.hxx> +#include <ShapeFactory.hxx> +#include <ObjectIdentifier.hxx> +#include <LabelPositionHelper.hxx> +#include <ChartType.hxx> + +#include <com/sun/star/chart/DataLabelPlacement.hpp> +#include <sal/log.hxx> +#include <osl/diagnose.h> + +#include <limits> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +BubbleChart::BubbleChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount, false ) + , m_fMaxLogicBubbleSize( 0.0 ) + , m_fBubbleSizeFactorToScreen( 1.0 ) +{ + // We only support 2 dimensional bubble charts + assert(nDimensionCount == 2); + + if( !m_pMainPosHelper ) + m_pMainPosHelper = new PlottingPositionHelper(); + PlotterBase::m_pPosHelper = m_pMainPosHelper; +} + +BubbleChart::~BubbleChart() +{ + delete m_pMainPosHelper; +} + +void BubbleChart::calculateMaximumLogicBubbleSize() +{ + double fMaxSize = 0.0; + + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + for( sal_Int32 nIndex = 0; nIndex < nEndIndex; nIndex++ ) + { + for( auto const& rZSlot : m_aZSlots ) + { + for( auto const& rXSlot : rZSlot ) + { + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + double fSize = pSeries->getBubble_Size( nIndex ); + if( fSize > fMaxSize ) + fMaxSize = fSize; + } + } + } + } + + m_fMaxLogicBubbleSize = fMaxSize; +} + +void BubbleChart::calculateBubbleSizeScalingFactor() +{ + double fLogicZ=1.0; + drawing::Position3D aSceneMinPos( m_pMainPosHelper->transformLogicToScene( m_pMainPosHelper->getLogicMinX(),m_pMainPosHelper->getLogicMinY(),fLogicZ, false ) ); + drawing::Position3D aSceneMaxPos( m_pMainPosHelper->transformLogicToScene( m_pMainPosHelper->getLogicMaxX(),m_pMainPosHelper->getLogicMaxY(),fLogicZ, false ) ); + + awt::Point aScreenMinPos( LabelPositionHelper(m_nDimension,m_xLogicTarget).transformSceneToScreenPosition( aSceneMinPos ) ); + awt::Point aScreenMaxPos( LabelPositionHelper(m_nDimension,m_xLogicTarget).transformSceneToScreenPosition( aSceneMaxPos ) ); + + sal_Int32 nWidth = abs( aScreenMaxPos.X - aScreenMinPos.X ); + sal_Int32 nHeight = abs( aScreenMaxPos.Y - aScreenMinPos.Y ); + + sal_Int32 nMinExtend = std::min( nWidth, nHeight ); + m_fBubbleSizeFactorToScreen = nMinExtend * 0.25;//max bubble size is 25 percent of diagram size +} + +drawing::Direction3D BubbleChart::transformToScreenBubbleSize( double fLogicSize ) +{ + drawing::Direction3D aRet(0,0,0); + + if( std::isnan(fLogicSize) || std::isinf(fLogicSize) ) + return aRet; + + double fMaxSize = m_fMaxLogicBubbleSize; + + double fMaxRadius = sqrt( fMaxSize / M_PI ); + double fRadius = sqrt( fLogicSize / M_PI ); + + aRet.DirectionX = m_fBubbleSizeFactorToScreen * fRadius / fMaxRadius; + aRet.DirectionY = aRet.DirectionX; + + return aRet; +} + +bool BubbleChart::isExpandIfValuesCloseToBorder( sal_Int32 /*nDimensionIndex*/ ) +{ + return true; +} + +bool BubbleChart::isSeparateStackingForDifferentSigns( sal_Int32 /*nDimensionIndex*/ ) +{ + return false; +} + +LegendSymbolStyle BubbleChart::getLegendSymbolStyle() +{ + return LegendSymbolStyle::Circle; +} + +drawing::Direction3D BubbleChart::getPreferredDiagramAspectRatio() const +{ + return drawing::Direction3D(-1,-1,-1); +} + +namespace { + +//better performance for big data +struct FormerPoint +{ + FormerPoint( double fX, double fY, double fZ ) + : m_fX(fX), m_fY(fY), m_fZ(fZ) + {} + FormerPoint() + : m_fX(std::numeric_limits<double>::quiet_NaN()) + , m_fY(std::numeric_limits<double>::quiet_NaN()) + , m_fZ(std::numeric_limits<double>::quiet_NaN()) + { + } + + double m_fX; + double m_fY; + double m_fZ; +}; + +} + +void BubbleChart::createShapes() +{ + if( m_aZSlots.empty() ) //no series + return; + + OSL_ENSURE(m_xLogicTarget.is()&&m_xFinalTarget.is(),"BubbleChart is not proper initialized"); + if(!(m_xLogicTarget.is()&&m_xFinalTarget.is())) + return; + + //therefore create an own group for the texts and the error bars 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 ); + + //update/create information for current group + double fLogicZ = 1.0;//as defined + + sal_Int32 const nStartIndex = 0; // inclusive ;..todo get somehow from x scale + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + if(nEndIndex<=0) + nEndIndex=1; + + //better performance for big data + std::map< VDataSeries*, FormerPoint > aSeriesFormerPointMap; + m_bPointsWereSkipped = false; + sal_Int32 nSkippedPoints = 0; + sal_Int32 nCreatedPoints = 0; + + calculateMaximumLogicBubbleSize(); + calculateBubbleSizeScalingFactor(); + if( m_fMaxLogicBubbleSize <= 0 || m_fBubbleSizeFactorToScreen <= 0 ) + return; + + //iterate through all x values per indices + for( sal_Int32 nIndex = nStartIndex; nIndex < nEndIndex; nIndex++ ) + { + for( auto const& rZSlot : m_aZSlots ) + { + for( auto const& rXSlot : rZSlot ) + { + //iterate through all series + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + bool bHasFillColorMapping = pSeries->hasPropertyMapping("FillColor"); + bool bHasBorderColorMapping = pSeries->hasPropertyMapping("LineColor"); + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShape(pSeries.get(), xSeriesTarget); + + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + PlottingPositionHelper& rPosHelper + = getPlottingPositionHelper(nAttachedAxisIndex); + m_pPosHelper = &rPosHelper; + + //collect data point information (logic coordinates, style ): + double fLogicX = pSeries->getXValue(nIndex); + double fLogicY = pSeries->getYValue(nIndex); + double fBubbleSize = pSeries->getBubble_Size( nIndex ); + + if( fBubbleSize<0.0 ) + continue; + + if( fBubbleSize == 0.0 || std::isnan(fBubbleSize) ) + continue; + + if( std::isnan(fLogicX) || std::isinf(fLogicX) + || std::isnan(fLogicY) || std::isinf(fLogicY) ) + continue; + + bool bIsVisible = rPosHelper.isLogicVisible(fLogicX, fLogicY, fLogicZ); + + drawing::Position3D aUnscaledLogicPosition( fLogicX, fLogicY, fLogicZ ); + drawing::Position3D aScaledLogicPosition(aUnscaledLogicPosition); + rPosHelper.doLogicScaling(aScaledLogicPosition); + + //transformation 3) -> 4) + drawing::Position3D aScenePosition( + rPosHelper.transformLogicToScene(fLogicX, fLogicY, fLogicZ, false)); + + //better performance for big data + FormerPoint aFormerPoint( aSeriesFormerPointMap[pSeries.get()] ); + rPosHelper.setCoordinateSystemResolution(m_aCoordinateSystemResolution); + if (!pSeries->isAttributedDataPoint(nIndex) + && rPosHelper.isSameForGivenResolution( + aFormerPoint.m_fX, aFormerPoint.m_fY, aFormerPoint.m_fZ, + aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, + aScaledLogicPosition.PositionZ)) + { + nSkippedPoints++; + m_bPointsWereSkipped = true; + continue; + } + aSeriesFormerPointMap[pSeries.get()] = FormerPoint(aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, aScaledLogicPosition.PositionZ); + + //create a single datapoint if point is visible + if( !bIsVisible ) + continue; + + //create a group shape for this point and add to the series shape: + OUString aPointCID = ObjectIdentifier::createPointCID( + pSeries->getPointCID_Stub(), nIndex ); + rtl::Reference<SvxShapeGroupAnyD> xPointGroupShape_Shapes( + createGroupShape(xSeriesGroupShape_Shapes,aPointCID) ); + + { + nCreatedPoints++; + + //create data point + drawing::Direction3D aSymbolSize = transformToScreenBubbleSize( fBubbleSize ); + rtl::Reference<SvxShapeCircle> xShape = ShapeFactory::createCircle2D( xPointGroupShape_Shapes + , aScenePosition, aSymbolSize ); + + PropertyMapper::setMappedProperties( *xShape + , pSeries->getPropertiesOfPoint( nIndex ) + , PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + + if(bHasFillColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nIndex, "FillColor"); + if(!std::isnan(nPropVal)) + { + xShape->SvxShape::setPropertyValue("FillColor", uno::Any(static_cast<sal_Int32>(nPropVal))); + } + } + if(bHasBorderColorMapping) + { + double nPropVal = pSeries->getValueByProperty(nIndex, "LineColor"); + if(!std::isnan(nPropVal)) + { + xShape->SvxShape::setPropertyValue("LineColor", uno::Any(static_cast<sal_Int32>(nPropVal))); + } + } + + ::chart::ShapeFactory::setShapeName( xShape, "MarkHandles" ); + + //create data point label + if( pSeries->getDataPointLabelIfLabel(nIndex) ) + { + LabelAlignment eAlignment = LABEL_ALIGN_TOP; + drawing::Position3D aScenePosition3D( aScenePosition.PositionX + , aScenePosition.PositionY + , aScenePosition.PositionZ+getTransformedDepth() ); + + sal_Int32 nLabelPlacement = pSeries->getLabelPlacement( + nIndex, m_xChartTypeModel, rPosHelper.isSwapXAndY()); + + switch(nLabelPlacement) + { + case css::chart::DataLabelPlacement::TOP: + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + case css::chart::DataLabelPlacement::BOTTOM: + aScenePosition3D.PositionY += (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_BOTTOM; + break; + case css::chart::DataLabelPlacement::LEFT: + aScenePosition3D.PositionX -= (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_LEFT; + break; + case css::chart::DataLabelPlacement::RIGHT: + aScenePosition3D.PositionX += (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_RIGHT; + break; + case css::chart::DataLabelPlacement::CENTER: + eAlignment = LABEL_ALIGN_CENTER; + break; + default: + OSL_FAIL("this label alignment is not implemented yet"); + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + } + + awt::Point aScreenPosition2D( LabelPositionHelper(m_nDimension,m_xLogicTarget) + .transformSceneToScreenPosition( aScenePosition3D ) ); + sal_Int32 nOffset = 0; + if(eAlignment!=LABEL_ALIGN_CENTER) + nOffset = 100;//add some spacing //@todo maybe get more intelligent values + createDataLabel( xTextTarget, *pSeries, nIndex + , fBubbleSize, fBubbleSize, aScreenPosition2D, eAlignment, nOffset ); + } + } + + //remove PointGroupShape if empty + if(!xPointGroupShape_Shapes->getCount()) + xSeriesGroupShape_Shapes->remove(xPointGroupShape_Shapes); + + }//next series in x slot (next y slot) + }//next x slot + }//next z slot + }//next category + SAL_INFO( + "chart2", + "skipped points: " << nSkippedPoints << " created points: " + << nCreatedPoints); +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/BubbleChart.hxx b/chart2/source/view/charttypes/BubbleChart.hxx new file mode 100644 index 000000000..c25e5b634 --- /dev/null +++ b/chart2/source/view/charttypes/BubbleChart.hxx @@ -0,0 +1,60 @@ +/* -*- 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 . + */ +#pragma once + +#include <VSeriesPlotter.hxx> +#include <com/sun/star/drawing/Direction3D.hpp> + +namespace chart +{ + +class BubbleChart : public VSeriesPlotter +{ + // public methods +public: + BubbleChart() = delete; + + BubbleChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount ); + virtual ~BubbleChart() override; + + virtual void createShapes() override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + + // MinimumAndMaximumSupplier + virtual bool isExpandIfValuesCloseToBorder( sal_Int32 nDimensionIndex ) override; + virtual bool isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) override; + + virtual LegendSymbolStyle getLegendSymbolStyle() override; + +private: //methods + void calculateMaximumLogicBubbleSize(); + void calculateBubbleSizeScalingFactor(); + + css::drawing::Direction3D transformToScreenBubbleSize( double fLogicSize ); + +private: //member + + double m_fMaxLogicBubbleSize;//calculated values + double m_fBubbleSizeFactorToScreen;//calculated values +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/CandleStickChart.cxx b/chart2/source/view/charttypes/CandleStickChart.cxx new file mode 100644 index 000000000..5c8497a5d --- /dev/null +++ b/chart2/source/view/charttypes/CandleStickChart.cxx @@ -0,0 +1,314 @@ +/* -*- 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 "CandleStickChart.hxx" +#include <ChartType.hxx> +#include <ShapeFactory.hxx> +#include <CommonConverters.hxx> +#include <ExplicitCategoriesProvider.hxx> +#include <ObjectIdentifier.hxx> +#include "BarPositionHelper.hxx" +#include <DateHelper.hxx> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <tools/diagnose_ex.h> +#include <osl/diagnose.h> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +CandleStickChart::CandleStickChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount ) + , m_pMainPosHelper( new BarPositionHelper() ) +{ + PlotterBase::m_pPosHelper = m_pMainPosHelper.get(); + VSeriesPlotter::m_pMainPosHelper = m_pMainPosHelper.get(); +} + +CandleStickChart::~CandleStickChart() +{ +} + +// MinimumAndMaximumSupplier + +bool CandleStickChart::isSeparateStackingForDifferentSigns( sal_Int32 /* nDimensionIndex */ ) +{ + return false; +} + +LegendSymbolStyle CandleStickChart::getLegendSymbolStyle() +{ + return LegendSymbolStyle::Line; +} + +drawing::Direction3D CandleStickChart::getPreferredDiagramAspectRatio() const +{ + return drawing::Direction3D(-1,-1,-1); +} + +void CandleStickChart::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 /* zSlot */, sal_Int32 xSlot, sal_Int32 ySlot ) +{ + //ignore y stacking for candle stick chart + VSeriesPlotter::addSeries( std::move(pSeries), 0, xSlot, ySlot ); +} + +void CandleStickChart::createShapes() +{ + if( m_aZSlots.empty() ) //no series + return; + + if( m_nDimension!=2 ) + return; + + OSL_ENSURE(m_xLogicTarget.is()&&m_xFinalTarget.is(),"CandleStickChart is not proper 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<SvxShapeGroupAnyD> xLossTarget = + createGroupShape( m_xLogicTarget, ObjectIdentifier::createClassifiedIdentifier( + OBJECTTYPE_DATA_STOCK_LOSS, u"" )); + rtl::Reference<SvxShapeGroupAnyD> xGainTarget = + createGroupShape( m_xLogicTarget, ObjectIdentifier::createClassifiedIdentifier( + OBJECTTYPE_DATA_STOCK_GAIN, u"" )); + rtl::Reference< SvxShapeGroup > xTextTarget = + ShapeFactory::createGroup2D( m_xFinalTarget ); + + //check necessary here that different Y axis can not be stacked in the same group? ... hm? + + bool bJapaneseStyle=true;//@todo is this the correct default? + bool bShowFirst = true;//is only important if bJapaneseStyle == false + tNameSequence aWhiteBox_Names, aBlackBox_Names; + tAnySequence aWhiteBox_Values, aBlackBox_Values; + try + { + if( m_xChartTypeModel.is() ) + { + m_xChartTypeModel->getPropertyValue( "ShowFirst" ) >>= bShowFirst; + + uno::Reference< beans::XPropertySet > xWhiteDayProps; + uno::Reference< beans::XPropertySet > xBlackDayProps; + m_xChartTypeModel->getPropertyValue( "Japanese" ) >>= bJapaneseStyle; + m_xChartTypeModel->getPropertyValue( "WhiteDay" ) >>= xWhiteDayProps; + m_xChartTypeModel->getPropertyValue( "BlackDay" ) >>= xBlackDayProps; + + tPropertyNameValueMap aWhiteBox_Map; + PropertyMapper::getValueMap( aWhiteBox_Map, PropertyMapper::getPropertyNameMapForFillAndLineProperties(), xWhiteDayProps ); + PropertyMapper::getMultiPropertyListsFromValueMap( aWhiteBox_Names, aWhiteBox_Values, aWhiteBox_Map ); + + tPropertyNameValueMap aBlackBox_Map; + PropertyMapper::getValueMap( aBlackBox_Map, PropertyMapper::getPropertyNameMapForFillAndLineProperties(), xBlackDayProps ); + PropertyMapper::getMultiPropertyListsFromValueMap( aBlackBox_Names, aBlackBox_Values, aBlackBox_Map ); + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + //(@todo maybe different iteration for breaks in axis ?) + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + double fLogicZ = 1.5;//as defined + //iterate through all x values per indices + for( sal_Int32 nIndex = 0; nIndex < nEndIndex; nIndex++ ) + { + for( auto const& rZSlot : m_aZSlots ) + { + BarPositionHelper* pPosHelper = m_pMainPosHelper.get(); + if( !rZSlot.empty() ) + { + sal_Int32 nAttachedAxisIndex = rZSlot.front().getAttachedAxisIndexForFirstSeries(); + //2ND_AXIS_IN_BARS so far one can assume to have the same plotter for each z slot + pPosHelper = dynamic_cast<BarPositionHelper*>(&( getPlottingPositionHelper( nAttachedAxisIndex ) ) ); + if(!pPosHelper) + pPosHelper = m_pMainPosHelper.get(); + } + PlotterBase::m_pPosHelper = pPosHelper; + + //update/create information for current group + pPosHelper->updateSeriesCount( rZSlot.size() ); + double fSlotX=0; + //iterate through all x slots in this category + for( auto const& rXSlot : rZSlot ) + { + //iterate through all series in this x slot + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + //collect data point information (logic coordinates, style ): + double fUnscaledX = pSeries->getXValue( nIndex ); + if( m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis() ) + fUnscaledX = DateHelper::RasterizeDateValue( fUnscaledX, m_aNullDate, m_nTimeResolution ); + if(fUnscaledX<pPosHelper->getLogicMinX() || fUnscaledX>pPosHelper->getLogicMaxX()) + continue;//point not visible + double fScaledX = pPosHelper->getScaledSlotPos( fUnscaledX, fSlotX ); + + double fUnscaledY_First = pSeries->getY_First( nIndex ); + double fUnscaledY_Last = pSeries->getY_Last( nIndex ); + double fUnscaledY_Min = pSeries->getY_Min( nIndex ); + double fUnscaledY_Max = pSeries->getY_Max( nIndex ); + + bool bBlack=false; + if(fUnscaledY_Last<=fUnscaledY_First) + { + std::swap(fUnscaledY_First,fUnscaledY_Last); + bBlack=true; + } + if(fUnscaledY_Max<fUnscaledY_Min) + std::swap(fUnscaledY_Min,fUnscaledY_Max); + //transformation 3) -> 4) + double fHalfScaledWidth = pPosHelper->getScaledSlotWidth()/2.0; + + double fScaledY_First(fUnscaledY_First); + double fScaledY_Last(fUnscaledY_Last); + double fScaledY_Min(fUnscaledY_Min); + double fScaledY_Max(fUnscaledY_Max); + pPosHelper->clipLogicValues( nullptr,&fScaledY_First,nullptr ); + pPosHelper->clipLogicValues( nullptr,&fScaledY_Last,nullptr ); + pPosHelper->clipLogicValues( nullptr,&fScaledY_Min,nullptr ); + pPosHelper->clipLogicValues( nullptr,&fScaledY_Max,nullptr ); + pPosHelper->doLogicScaling( nullptr,&fScaledY_First,nullptr ); + pPosHelper->doLogicScaling( nullptr,&fScaledY_Last,nullptr ); + pPosHelper->doLogicScaling( nullptr,&fScaledY_Min,nullptr ); + pPosHelper->doLogicScaling( nullptr,&fScaledY_Max,nullptr ); + + drawing::Position3D aPosLeftFirst( pPosHelper->transformScaledLogicToScene( fScaledX-fHalfScaledWidth, fScaledY_First ,0 ,true ) ); + drawing::Position3D aPosRightLast( pPosHelper->transformScaledLogicToScene( fScaledX+fHalfScaledWidth, fScaledY_Last ,0 ,true ) ); + drawing::Position3D aPosMiddleFirst( pPosHelper->transformScaledLogicToScene( fScaledX, fScaledY_First ,0 ,true ) ); + drawing::Position3D aPosMiddleLast( pPosHelper->transformScaledLogicToScene( fScaledX, fScaledY_Last ,0 ,true ) ); + drawing::Position3D aPosMiddleMinimum( pPosHelper->transformScaledLogicToScene( fScaledX, fScaledY_Min ,0 ,true ) ); + drawing::Position3D aPosMiddleMaximum( pPosHelper->transformScaledLogicToScene( fScaledX, fScaledY_Max ,0 ,true ) ); + + rtl::Reference<SvxShapeGroupAnyD> xLossGainTarget( xGainTarget ); + if(bBlack) + xLossGainTarget = xLossTarget; + + uno::Reference< beans::XPropertySet > xPointProp( pSeries->getPropertiesOfPoint( nIndex )); + rtl::Reference<SvxShapeGroupAnyD> xPointGroupShape_Shapes; + { + OUString aPointCID = ObjectIdentifier::createPointCID( pSeries->getPointCID_Stub(), nIndex ); + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes( getSeriesGroupShape(pSeries.get(), xSeriesTarget) ); + xPointGroupShape_Shapes = createGroupShape(xSeriesGroupShape_Shapes,aPointCID); + } + + //create min-max line + if( isValidPosition(aPosMiddleMinimum) && isValidPosition(aPosMiddleMaximum) ) + { + std::vector<std::vector<css::drawing::Position3D>> aPoly + { + { aPosMiddleMinimum, aPosMiddleMaximum } + }; + + rtl::Reference<SvxShapePolyPolygon> xShape = + ShapeFactory::createLine2D( xPointGroupShape_Shapes, aPoly); + PropertyMapper::setMappedProperties( *xShape, xPointProp, PropertyMapper::getPropertyNameMapForLineSeriesProperties() ); + } + + //create first-last shape + if(bJapaneseStyle && isValidPosition(aPosLeftFirst) && isValidPosition(aPosRightLast) ) + { + drawing::Direction3D aDiff = aPosRightLast-aPosLeftFirst; + awt::Size aAWTSize( Direction3DToAWTSize( aDiff )); + // workaround for bug in drawing: if height is 0 the box gets infinitely large + if( aAWTSize.Height == 0 ) + aAWTSize.Height = 1; + + tNameSequence aNames; + tAnySequence aValues; + + rtl::Reference<SvxShapeRect> xShape = + ShapeFactory::createRectangle( xLossGainTarget, + aAWTSize, Position3DToAWTPoint( aPosLeftFirst ), + aNames, aValues); + + if(bBlack) + PropertyMapper::setMultiProperties( aBlackBox_Names, aBlackBox_Values, *xShape ); + else + PropertyMapper::setMultiProperties( aWhiteBox_Names, aWhiteBox_Values, *xShape ); + } + else + { + std::vector<std::vector<css::drawing::Position3D>> aPoly; + + sal_Int32 nLineIndex = 0; + if( bShowFirst && pPosHelper->isLogicVisible( fUnscaledX, fUnscaledY_First ,fLogicZ ) + && isValidPosition(aPosLeftFirst) && isValidPosition(aPosMiddleFirst) ) + { + AddPointToPoly( aPoly, aPosLeftFirst, nLineIndex ); + AddPointToPoly( aPoly, aPosMiddleFirst, nLineIndex++ ); + } + if( pPosHelper->isLogicVisible( fUnscaledX, fUnscaledY_Last ,fLogicZ ) + && isValidPosition(aPosMiddleLast) && isValidPosition(aPosRightLast) ) + { + AddPointToPoly( aPoly, aPosMiddleLast, nLineIndex ); + AddPointToPoly( aPoly, aPosRightLast, nLineIndex ); + } + + if( !aPoly.empty() ) + { + rtl::Reference<SvxShapePolyPolygon> xShape = + ShapeFactory::createLine2D( xPointGroupShape_Shapes, aPoly ); + PropertyMapper::setMappedProperties( *xShape, xPointProp, PropertyMapper::getPropertyNameMapForLineSeriesProperties() ); + } + } + + //create data point label + if( pSeries->getDataPointLabelIfLabel(nIndex) ) + { + if(isValidPosition(aPosMiddleFirst)) + createDataLabel( xTextTarget, *pSeries, nIndex + , fUnscaledY_First, 1.0, Position3DToAWTPoint(aPosMiddleFirst), LABEL_ALIGN_LEFT_BOTTOM ); + if(isValidPosition(aPosMiddleLast)) + createDataLabel( xTextTarget, *pSeries, nIndex + , fUnscaledY_Last, 1.0, Position3DToAWTPoint(aPosMiddleLast), LABEL_ALIGN_RIGHT_TOP ); + if(isValidPosition(aPosMiddleMinimum)) + createDataLabel( xTextTarget, *pSeries, nIndex + , fUnscaledY_Min, 1.0, Position3DToAWTPoint(aPosMiddleMinimum), LABEL_ALIGN_BOTTOM ); + if(isValidPosition(aPosMiddleMaximum)) + createDataLabel( xTextTarget, *pSeries, nIndex + , fUnscaledY_Max, 1.0, Position3DToAWTPoint(aPosMiddleMaximum), LABEL_ALIGN_TOP ); + } + }//next series in x slot (next y slot) + fSlotX+=1.0; + }//next x slot + }//next z slot + }//next category + /* @todo remove series shapes if empty + //remove and delete point-group-shape if empty + if(!xSeriesGroupShape_Shapes->getCount()) + { + pSeries->m_xShape.set(NULL); + m_xLogicTarget->remove(xSeriesGroupShape_Shape); + } + */ + + //remove and delete series-group-shape if empty + + //... todo +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/CandleStickChart.hxx b/chart2/source/view/charttypes/CandleStickChart.hxx new file mode 100644 index 000000000..93571889e --- /dev/null +++ b/chart2/source/view/charttypes/CandleStickChart.hxx @@ -0,0 +1,54 @@ +/* -*- 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 . + */ + +#pragma once + +#include <memory> +#include <VSeriesPlotter.hxx> + +namespace chart +{ +class BarPositionHelper; + +class CandleStickChart : public VSeriesPlotter +{ + // public methods +public: + CandleStickChart() = delete; + + CandleStickChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount ); + virtual ~CandleStickChart() override; + + virtual void createShapes() override; + virtual void addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + + // MinimumAndMaximumSupplier + virtual bool isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) override; + + virtual LegendSymbolStyle getLegendSymbolStyle() override; + +private: //member + std::unique_ptr<BarPositionHelper> m_pMainPosHelper; +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/CategoryPositionHelper.cxx b/chart2/source/view/charttypes/CategoryPositionHelper.cxx new file mode 100644 index 000000000..d7412d3cb --- /dev/null +++ b/chart2/source/view/charttypes/CategoryPositionHelper.cxx @@ -0,0 +1,82 @@ +/* -*- 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 "CategoryPositionHelper.hxx" + +namespace chart +{ + +CategoryPositionHelper::CategoryPositionHelper( double fSeriesCount, double fCategoryWidth ) + : m_fSeriesCount(fSeriesCount) + , m_fCategoryWidth(fCategoryWidth) + , m_fInnerDistance(0.0) + , m_fOuterDistance(1.0) +{ +} + +CategoryPositionHelper::~CategoryPositionHelper() +{ +} + +double CategoryPositionHelper::getScaledSlotWidth() const +{ + double fWidth = m_fCategoryWidth / + ( m_fSeriesCount + + m_fOuterDistance + + m_fInnerDistance*( m_fSeriesCount - 1.0) ); + return fWidth; +} + +double CategoryPositionHelper::getScaledSlotPos( double fScaledXPos, double fSeriesNumber ) const +{ + //the returned position is in the middle of the rect + //fSeriesNumber 0...n-1 + double fPos = fScaledXPos + - (m_fCategoryWidth/2.0) + + (m_fOuterDistance/2.0 + fSeriesNumber*(1.0+m_fInnerDistance)) * getScaledSlotWidth() + + getScaledSlotWidth()/2.0; + return fPos; +} + +void CategoryPositionHelper::setInnerDistance( double fInnerDistance ) +{ + if( fInnerDistance < -1.0 ) + fInnerDistance = -1.0; + if( fInnerDistance > 1.0 ) + fInnerDistance = 1.0; + m_fInnerDistance = fInnerDistance; +} + +void CategoryPositionHelper::setOuterDistance( double fOuterDistance ) +{ + if( fOuterDistance < 0.0 ) + fOuterDistance = 0.0; + if( fOuterDistance > 6.0 ) + fOuterDistance = 6.0; + m_fOuterDistance = fOuterDistance; +} + +void CategoryPositionHelper::setCategoryWidth( double fCategoryWidth ) +{ + m_fCategoryWidth = fCategoryWidth; +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/CategoryPositionHelper.hxx b/chart2/source/view/charttypes/CategoryPositionHelper.hxx new file mode 100644 index 000000000..ebc784ef6 --- /dev/null +++ b/chart2/source/view/charttypes/CategoryPositionHelper.hxx @@ -0,0 +1,57 @@ +/* -*- 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 . + */ + +#pragma once + +namespace chart +{ +class CategoryPositionHelper +{ +public: + CategoryPositionHelper(double fSeriesCount, double CategoryWidth = 1.0); + virtual ~CategoryPositionHelper(); + + CategoryPositionHelper(CategoryPositionHelper const&) = default; + CategoryPositionHelper(CategoryPositionHelper&&) = default; + CategoryPositionHelper& operator=(CategoryPositionHelper const&) = default; + CategoryPositionHelper& operator=(CategoryPositionHelper&&) = default; + + double getScaledSlotWidth() const; + virtual double getScaledSlotPos(double fCategoryX, double fSeriesNumber) const; + void setCategoryWidth(double fCategoryWidth); + + //Distance between two neighboring bars in same category, seen relative to width of the bar + void setInnerDistance(double fInnerDistance); + + //Distance between two neighboring bars in different category, seen relative to width of the bar: + void setOuterDistance(double fOuterDistance); + +protected: + double m_fSeriesCount; + double m_fCategoryWidth; + //Distance between two neighboring bars in same category, seen relative to width of the bar: + double + m_fInnerDistance; //[-1,1] m_fInnerDistance=1 --> distance == width; m_fInnerDistance=-1-->all rects are painted on the same position + //Distance between two neighboring bars in different category, seen relative to width of the bar: + double m_fOuterDistance; //>=0 m_fOuterDistance=1 --> distance == width +}; + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/ConfigAccess.cxx b/chart2/source/view/charttypes/ConfigAccess.cxx new file mode 100644 index 000000000..ce02c4817 --- /dev/null +++ b/chart2/source/view/charttypes/ConfigAccess.cxx @@ -0,0 +1,74 @@ +/* -*- 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 <ConfigAccess.hxx> + +#include <unotools/configitem.hxx> +#include <o3tl/any.hxx> +#include <com/sun/star/uno/Sequence.hxx> + +namespace chart +{ +using namespace ::com::sun::star; + +namespace +{ +class ChartConfigItem : public ::utl::ConfigItem +{ +private: + virtual void ImplCommit() override; + +public: + ChartConfigItem(); + + bool getUseErrorRectangle(); + virtual void Notify(const uno::Sequence<OUString>& aPropertyNames) override; +}; +} + +ChartConfigItem::ChartConfigItem() + : ConfigItem("Office.Chart/ErrorProperties") +{ +} + +void ChartConfigItem::ImplCommit() {} +void ChartConfigItem::Notify(const uno::Sequence<OUString>&) {} + +bool ChartConfigItem::getUseErrorRectangle() +{ + uno::Sequence<OUString> aNames{ "ErrorRectangle" }; + + auto b = o3tl::tryAccess<bool>(GetProperties(aNames)[0]); + return b && *b; +} + +namespace ConfigAccess +{ +bool getUseErrorRectangle() +{ + //a ChartConfigItem Singleton + static ChartConfigItem SINGLETON; + bool bResult(SINGLETON.getUseErrorRectangle()); + return bResult; +} +} //namespace ConfigAccess + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/NetChart.cxx b/chart2/source/view/charttypes/NetChart.cxx new file mode 100644 index 000000000..5b8f1db34 --- /dev/null +++ b/chart2/source/view/charttypes/NetChart.cxx @@ -0,0 +1,642 @@ +/* -*- 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 "NetChart.hxx" +#include <PlottingPositionHelper.hxx> +#include <ShapeFactory.hxx> +#include <ExplicitCategoriesProvider.hxx> +#include <CommonConverters.hxx> +#include <ObjectIdentifier.hxx> +#include <LabelPositionHelper.hxx> +#include <Clipping.hxx> +#include <PolarLabelPositionHelper.hxx> +#include <DateHelper.hxx> +#include <ChartType.hxx> + +#include <com/sun/star/chart2/Symbol.hpp> +#include <com/sun/star/chart/DataLabelPlacement.hpp> +#include <com/sun/star/chart/MissingValueTreatment.hpp> + +#include <o3tl/safeint.hxx> +#include <osl/diagnose.h> + +#include <officecfg/Office/Compatibility.hxx> + +#include <limits> + +namespace chart +{ +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart2; + +NetChart::NetChart( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bNoArea + , std::unique_ptr<PlottingPositionHelper> pPlottingPositionHelper + ) + : VSeriesPlotter( xChartTypeModel, nDimensionCount, true ) + , m_pMainPosHelper(std::move(pPlottingPositionHelper)) + , m_bArea(!bNoArea) + , m_bLine(bNoArea) +{ + // we only support 2D Net charts + assert(nDimensionCount == 2); + + m_pMainPosHelper->AllowShiftXAxisPos(true); + m_pMainPosHelper->AllowShiftZAxisPos(true); + + PlotterBase::m_pPosHelper = m_pMainPosHelper.get(); + VSeriesPlotter::m_pMainPosHelper = m_pMainPosHelper.get(); +} + +NetChart::~NetChart() +{ +} + +double NetChart::getMaximumX() +{ + double fMax = VSeriesPlotter::getMaximumX() + 1.0; + return fMax; +} + +bool NetChart::isExpandIfValuesCloseToBorder( sal_Int32 ) +{ + return false; +} + +bool NetChart::isSeparateStackingForDifferentSigns( sal_Int32 /*nDimensionIndex*/ ) +{ + // no separate stacking in all types of line/area charts + return false; +} + +LegendSymbolStyle NetChart::getLegendSymbolStyle() +{ + if( m_bArea ) + return LegendSymbolStyle::Box; + return LegendSymbolStyle::Line; +} + +uno::Any NetChart::getExplicitSymbol( const VDataSeries& rSeries, sal_Int32 nPointIndex ) +{ + uno::Any aRet; + + Symbol* pSymbolProperties = rSeries.getSymbolProperties( nPointIndex ); + if( pSymbolProperties ) + { + aRet <<= *pSymbolProperties; + } + + return aRet; +} + +drawing::Direction3D NetChart::getPreferredDiagramAspectRatio() const +{ + return drawing::Direction3D(1,1,1); +} + +bool NetChart::impl_createLine( VDataSeries* pSeries + , const std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly + , PlottingPositionHelper const * pPosHelper ) +{ + //return true if a line was created successfully + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeBackChild(pSeries, m_xSeriesTarget); + + std::vector<std::vector<css::drawing::Position3D>> aPoly; + { + bool bIsClipped = false; + if( !ShapeFactory::isPolygonEmptyOrSinglePoint(*pSeriesPoly) ) + { + // do NOT connect last and first point, if one is NAN, and NAN handling is NAN_AS_GAP + double fFirstY = pSeries->getYValue( 0 ); + double fLastY = pSeries->getYValue( VSeriesPlotter::getPointCount() - 1 ); + if( (pSeries->getMissingValueTreatment() != css::chart::MissingValueTreatment::LEAVE_GAP) + || (std::isfinite( fFirstY ) && std::isfinite( fLastY )) ) + { + // connect last point in last polygon with first point in first polygon + ::basegfx::B2DRectangle aScaledLogicClipDoubleRect( pPosHelper->getScaledLogicClipDoubleRect() ); + std::vector<std::vector<css::drawing::Position3D>> aTmpPoly(*pSeriesPoly); + drawing::Position3D aLast(aScaledLogicClipDoubleRect.getMaxX(),aTmpPoly[0][0].PositionY,aTmpPoly[0][0].PositionZ); + // add connector line to last polygon + AddPointToPoly( aTmpPoly, aLast, pSeriesPoly->size() - 1 ); + Clipping::clipPolygonAtRectangle( aTmpPoly, aScaledLogicClipDoubleRect, aPoly ); + bIsClipped = true; + } + } + + if( !bIsClipped ) + Clipping::clipPolygonAtRectangle( *pSeriesPoly, pPosHelper->getScaledLogicClipDoubleRect(), aPoly ); + } + + if(!ShapeFactory::hasPolygonAnyLines(aPoly)) + return false; + + //transformation 3) -> 4) + pPosHelper->transformScaledLogicToScene( aPoly ); + + //create line: + rtl::Reference<SvxShapePolyPolygon> xShape; + { + xShape = ShapeFactory::createLine2D( xSeriesGroupShape_Shapes, aPoly ); + PropertyMapper::setMappedProperties( *xShape + , pSeries->getPropertiesOfSeries() + , PropertyMapper::getPropertyNameMapForLineSeriesProperties() ); + //because of this name this line will be used for marking + ::chart::ShapeFactory::setShapeName(xShape, "MarkHandles"); + } + return true; +} + +bool NetChart::impl_createArea( VDataSeries* pSeries + , const std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly + , std::vector<std::vector<css::drawing::Position3D>> const * pPreviousSeriesPoly + , PlottingPositionHelper const * pPosHelper ) +{ + //return true if an area was created successfully + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeBackChild(pSeries, m_xSeriesTarget); + double zValue = pSeries->m_fLogicZPos; + + std::vector<std::vector<css::drawing::Position3D>> aPoly( *pSeriesPoly ); + //add second part to the polygon (grounding points or previous series points) + if( !ShapeFactory::isPolygonEmptyOrSinglePoint(*pSeriesPoly) ) + { + if( pPreviousSeriesPoly ) + addPolygon( aPoly, *pPreviousSeriesPoly ); + } + else if(!pPreviousSeriesPoly) + { + double fMinX = pSeries->m_fLogicMinX; + double fMaxX = pSeries->m_fLogicMaxX; + double fY = pPosHelper->getBaseValueY();//logic grounding + + //clip to scale + if(fMaxX<pPosHelper->getLogicMinX() || fMinX>pPosHelper->getLogicMaxX()) + return false;//no visible shape needed + pPosHelper->clipLogicValues( &fMinX, &fY, nullptr ); + pPosHelper->clipLogicValues( &fMaxX, nullptr, nullptr ); + + //apply scaling + { + pPosHelper->doLogicScaling( &fMinX, &fY, &zValue ); + pPosHelper->doLogicScaling( &fMaxX, nullptr, nullptr ); + } + + AddPointToPoly( aPoly, drawing::Position3D( fMaxX,fY,zValue) ); + AddPointToPoly( aPoly, drawing::Position3D( fMinX,fY,zValue) ); + } + else + { + appendPoly( aPoly, *pPreviousSeriesPoly ); + } + ShapeFactory::closePolygon(aPoly); + + //apply clipping + { + std::vector<std::vector<css::drawing::Position3D>> aClippedPoly; + Clipping::clipPolygonAtRectangle( aPoly, pPosHelper->getScaledLogicClipDoubleRect(), aClippedPoly, false ); + ShapeFactory::closePolygon(aClippedPoly); //again necessary after clipping + aPoly = aClippedPoly; + } + + if(!ShapeFactory::hasPolygonAnyLines(aPoly)) + return false; + + //transformation 3) -> 4) + pPosHelper->transformScaledLogicToScene( aPoly ); + + //create area: + rtl::Reference<SvxShapePolyPolygon> + xShape = ShapeFactory::createArea2D( xSeriesGroupShape_Shapes + , aPoly ); + PropertyMapper::setMappedProperties( *xShape + , pSeries->getPropertiesOfSeries() + , PropertyMapper::getPropertyNameMapForFilledSeriesProperties() ); + //because of this name this line will be used for marking + ::chart::ShapeFactory::setShapeName(xShape, "MarkHandles"); + return true; +} + +void NetChart::impl_createSeriesShapes() +{ + //the polygon shapes for each series need to be created before + + //iterate through all series again to create the series shapes + for( auto const& rZSlot : m_aZSlots ) + { + for( auto const& rXSlot : rZSlot ) + { + std::map< sal_Int32, std::vector<std::vector<css::drawing::Position3D>>* > aPreviousSeriesPolyMap;//a PreviousSeriesPoly for each different nAttachedAxisIndex + std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly = nullptr; + + //iterate through all series + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + m_pPosHelper = &getPlottingPositionHelper(nAttachedAxisIndex); + + pSeriesPoly = &pSeries->m_aPolyPolygonShape3D; + if( m_bArea ) + { + if (!impl_createArea(pSeries.get(), pSeriesPoly, + aPreviousSeriesPolyMap[nAttachedAxisIndex], m_pPosHelper)) + continue; + } + if( m_bLine ) + { + if (!impl_createLine(pSeries.get(), pSeriesPoly, m_pPosHelper)) + continue; + } + aPreviousSeriesPolyMap[nAttachedAxisIndex] = pSeriesPoly; + }//next series in x slot (next y slot) + }//next x slot + }//next z slot +} + +namespace +{ + +void lcl_reorderSeries( std::vector< std::vector< VDataSeriesGroup > >& rZSlots ) +{ + std::vector< std::vector< VDataSeriesGroup > > aRet; + aRet.reserve( rZSlots.size() ); + + std::vector< std::vector< VDataSeriesGroup > >::reverse_iterator aZIt( rZSlots.rbegin() ); + std::vector< std::vector< VDataSeriesGroup > >::reverse_iterator aZEnd( rZSlots.rend() ); + for( ; aZIt != aZEnd; ++aZIt ) + { + std::vector< VDataSeriesGroup > aXSlot; + + std::vector< VDataSeriesGroup >::reverse_iterator aXIt( aZIt->rbegin() ); + std::vector< VDataSeriesGroup >::reverse_iterator aXEnd( aZIt->rend() ); + for( ; aXIt != aXEnd; ++aXIt ) + aXSlot.push_back(std::move(*aXIt)); + + aRet.push_back(std::move(aXSlot)); + } + + rZSlots = std::move(aRet); +} + +//better performance for big data +struct FormerPoint +{ + FormerPoint( double fX, double fY, double fZ ) + : m_fX(fX), m_fY(fY), m_fZ(fZ) + {} + FormerPoint() + : m_fX(std::numeric_limits<double>::quiet_NaN()) + , m_fY(std::numeric_limits<double>::quiet_NaN()) + , m_fZ(std::numeric_limits<double>::quiet_NaN()) + + { + } + + double m_fX; + double m_fY; + double m_fZ; +}; + +}//anonymous namespace + +void NetChart::createShapes() +{ + if( m_aZSlots.empty() ) //no series + return; + + //tdf#127813 Don't reverse the series in OOXML-heavy environments + if (officecfg::Office::Compatibility::View::ReverseSeriesOrderAreaAndNetChart::get() && m_bArea) + lcl_reorderSeries( m_aZSlots ); + + OSL_ENSURE(m_xLogicTarget.is()&&m_xFinalTarget.is(),"NetChart is not proper initialized"); + if(!(m_xLogicTarget.is()&&m_xFinalTarget.is())) + return; + + //the text labels should be always on top of the other series shapes + //for area chart the error bars should be always on top of the other series shapes + + //therefore create an own group for the texts and the error bars to move them to front + //(because the text group is created after the series group the texts are displayed on top) + m_xSeriesTarget = createGroupShape( m_xLogicTarget ); + m_xTextTarget = ShapeFactory::createGroup2D( m_xFinalTarget ); + + //check necessary here that different Y axis can not be stacked in the same group? ... hm? + + //update/create information for current group + double fLogicZ = 1.0;//as defined + + sal_Int32 const nStartIndex = 0; // inclusive ;..todo get somehow from x scale + sal_Int32 nEndIndex = VSeriesPlotter::getPointCount(); + if(nEndIndex<=0) + nEndIndex=1; + + //better performance for big data + std::map< VDataSeries*, FormerPoint > aSeriesFormerPointMap; + m_bPointsWereSkipped = false; + + bool bDateCategory = (m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis()); + + //iterate through all x values per indices + for( sal_Int32 nIndex = nStartIndex; nIndex < nEndIndex; nIndex++ ) + { + std::map< sal_Int32, double > aLogicYSumMap;//one for each different nAttachedAxisIndex + for( auto const& rZSlot : m_aZSlots ) + { + //iterate through all x slots in this category to get 100percent sum + for( auto const& rXSlot : rZSlot ) + { + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + if (bDateCategory) + pSeries->doSortByXValues(); + + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + aLogicYSumMap.insert({nAttachedAxisIndex, 0.0}); + + m_pPosHelper = &getPlottingPositionHelper(nAttachedAxisIndex); + + double fAdd = pSeries->getYValue( nIndex ); + if( !std::isnan(fAdd) && !std::isinf(fAdd) ) + aLogicYSumMap[nAttachedAxisIndex] += fabs( fAdd ); + } + } + } + + for( auto const& rZSlot : m_aZSlots ) + { + //for the area chart there should be at most one x slot (no side by side stacking available) + //attention different: xSlots are always interpreted as independent areas one behind the other: @todo this doesn't work why not??? + for( auto const& rXSlot : rZSlot ) + { + std::map< sal_Int32, double > aLogicYForNextSeriesMap;//one for each different nAttachedAxisIndex + //iterate through all series + for( std::unique_ptr<VDataSeries> const & pSeries : rXSlot.m_aSeriesVector ) + { + if(!pSeries) + continue; + + /* #i70133# ignore points outside of series length in standard area + charts. Stacked area charts will use missing points as zeros. In + standard charts, pSeriesList contains only one series. */ + if( m_bArea && (rXSlot.m_aSeriesVector.size() == 1) && (nIndex >= pSeries->getTotalPointCount()) ) + continue; + + rtl::Reference<SvxShapeGroupAnyD> xSeriesGroupShape_Shapes = getSeriesGroupShapeFrontChild(pSeries.get(), m_xSeriesTarget); + + sal_Int32 nAttachedAxisIndex = pSeries->getAttachedAxisIndex(); + m_pPosHelper = &getPlottingPositionHelper(nAttachedAxisIndex); + + pSeries->m_fLogicZPos = fLogicZ; + + //collect data point information (logic coordinates, style ): + double fLogicX = pSeries->getXValue(nIndex); + if (bDateCategory) + fLogicX = DateHelper::RasterizeDateValue( fLogicX, m_aNullDate, m_nTimeResolution ); + double fLogicY = pSeries->getYValue(nIndex); + + if( m_bArea && ( std::isnan(fLogicY) || std::isinf(fLogicY) ) ) + { + if( pSeries->getMissingValueTreatment() == css::chart::MissingValueTreatment::LEAVE_GAP ) + { + if( rXSlot.m_aSeriesVector.size() == 1 || pSeries == rXSlot.m_aSeriesVector.front() ) + { + fLogicY = m_pPosHelper->getLogicMinY(); + if (!m_pPosHelper->isMathematicalOrientationY()) + fLogicY = m_pPosHelper->getLogicMaxY(); + } + else + fLogicY = 0.0; + } + } + + if (m_pPosHelper->isPercentY() && aLogicYSumMap[nAttachedAxisIndex] != 0.0) + { + fLogicY = fabs( fLogicY )/aLogicYSumMap[nAttachedAxisIndex]; + } + + if( std::isnan(fLogicX) || std::isinf(fLogicX) + || std::isnan(fLogicY) || std::isinf(fLogicY) + || std::isnan(fLogicZ) || std::isinf(fLogicZ) ) + { + if( pSeries->getMissingValueTreatment() == css::chart::MissingValueTreatment::LEAVE_GAP ) + { + std::vector<std::vector<css::drawing::Position3D>>& rPolygon = pSeries->m_aPolyPolygonShape3D; + sal_Int32& rIndex = pSeries->m_nPolygonIndex; + if( 0<= rIndex && o3tl::make_unsigned(rIndex) < rPolygon.size() ) + { + if( !rPolygon[ rIndex ].empty() ) + rIndex++; //start a new polygon for the next point if the current poly is not empty + } + } + continue; + } + + aLogicYForNextSeriesMap.try_emplace(nAttachedAxisIndex, 0.0); + + double fLogicValueForLabeDisplay = fLogicY; + + fLogicY += aLogicYForNextSeriesMap[nAttachedAxisIndex]; + aLogicYForNextSeriesMap[nAttachedAxisIndex] = fLogicY; + + bool bIsVisible = m_pPosHelper->isLogicVisible(fLogicX, fLogicY, fLogicZ); + + //remind minimal and maximal x values for area 'grounding' points + //only for filled area + { + double& rfMinX = pSeries->m_fLogicMinX; + if(!nIndex||fLogicX<rfMinX) + rfMinX=fLogicX; + double& rfMaxX = pSeries->m_fLogicMaxX; + if(!nIndex||fLogicX>rfMaxX) + rfMaxX=fLogicX; + } + + drawing::Position3D aUnscaledLogicPosition( fLogicX, fLogicY, fLogicZ ); + drawing::Position3D aScaledLogicPosition(aUnscaledLogicPosition); + m_pPosHelper->doLogicScaling(aScaledLogicPosition); + + //transformation 3) -> 4) + drawing::Position3D aScenePosition( + m_pPosHelper->transformLogicToScene(fLogicX, fLogicY, fLogicZ, false)); + + //better performance for big data + FormerPoint aFormerPoint( aSeriesFormerPointMap[pSeries.get()] ); + m_pPosHelper->setCoordinateSystemResolution(m_aCoordinateSystemResolution); + if( !pSeries->isAttributedDataPoint(nIndex) + && m_pPosHelper->isSameForGivenResolution( + aFormerPoint.m_fX, aFormerPoint.m_fY, aFormerPoint.m_fZ + , aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, aScaledLogicPosition.PositionZ ) ) + { + m_bPointsWereSkipped = true; + continue; + } + aSeriesFormerPointMap[pSeries.get()] = FormerPoint(aScaledLogicPosition.PositionX, aScaledLogicPosition.PositionY, aScaledLogicPosition.PositionZ); + + //store point information for series polygon + //for area and/or line (symbols only do not need this) + if( isValidPosition(aScaledLogicPosition) ) + { + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aScaledLogicPosition, pSeries->m_nPolygonIndex ); + + //prepare clipping for filled net charts + if( !bIsVisible && m_bArea ) + { + drawing::Position3D aClippedPos(aScaledLogicPosition); + m_pPosHelper->clipScaledLogicValues(nullptr, &aClippedPos.PositionY, + nullptr); + if (m_pPosHelper->isLogicVisible(aClippedPos.PositionX, + aClippedPos.PositionY, + aClippedPos.PositionZ)) + { + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aClippedPos, pSeries->m_nPolygonIndex ); + AddPointToPoly( pSeries->m_aPolyPolygonShape3D, aScaledLogicPosition, pSeries->m_nPolygonIndex ); + } + } + } + + //create a single datapoint if point is visible + //apply clipping: + if( !bIsVisible ) + continue; + + Symbol* pSymbolProperties = pSeries->getSymbolProperties( nIndex ); + bool bCreateSymbol = pSymbolProperties && (pSymbolProperties->Style != SymbolStyle_NONE); + + if( !bCreateSymbol && !pSeries->getDataPointLabelIfLabel(nIndex) ) + continue; + + //create a group shape for this point and add to the series shape: + OUString aPointCID = ObjectIdentifier::createPointCID( + pSeries->getPointCID_Stub(), nIndex ); + rtl::Reference<SvxShapeGroupAnyD> xPointGroupShape_Shapes( + createGroupShape(xSeriesGroupShape_Shapes,aPointCID) ); + + { + //create data point + drawing::Direction3D aSymbolSize(0,0,0); + if (bCreateSymbol) // implies pSymbolProperties + { + if (pSymbolProperties->Style != SymbolStyle_NONE) + { + aSymbolSize.DirectionX = pSymbolProperties->Size.Width; + aSymbolSize.DirectionY = pSymbolProperties->Size.Height; + } + + if (pSymbolProperties->Style == SymbolStyle_STANDARD) + { + sal_Int32 nSymbol = pSymbolProperties->StandardSymbol; + ShapeFactory::createSymbol2D( + xPointGroupShape_Shapes, aScenePosition, aSymbolSize, nSymbol, + pSymbolProperties->BorderColor, pSymbolProperties->FillColor); + } + else if (pSymbolProperties->Style == SymbolStyle_GRAPHIC) + { + ShapeFactory::createGraphic2D(xPointGroupShape_Shapes, + aScenePosition, aSymbolSize, + pSymbolProperties->Graphic); + } + //@todo other symbol styles + } + + //create data point label + if( pSeries->getDataPointLabelIfLabel(nIndex) ) + { + LabelAlignment eAlignment = LABEL_ALIGN_TOP; + drawing::Position3D aScenePosition3D( aScenePosition.PositionX + , aScenePosition.PositionY + , aScenePosition.PositionZ+getTransformedDepth() ); + + sal_Int32 nLabelPlacement = pSeries->getLabelPlacement( + nIndex, m_xChartTypeModel, m_pPosHelper->isSwapXAndY()); + + switch(nLabelPlacement) + { + case css::chart::DataLabelPlacement::TOP: + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + case css::chart::DataLabelPlacement::BOTTOM: + aScenePosition3D.PositionY += (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_BOTTOM; + break; + case css::chart::DataLabelPlacement::LEFT: + aScenePosition3D.PositionX -= (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_LEFT; + break; + case css::chart::DataLabelPlacement::RIGHT: + aScenePosition3D.PositionX += (aSymbolSize.DirectionX/2+1); + eAlignment = LABEL_ALIGN_RIGHT; + break; + case css::chart::DataLabelPlacement::CENTER: + eAlignment = LABEL_ALIGN_CENTER; + //todo implement this different for area charts + break; + default: + OSL_FAIL("this label alignment is not implemented yet"); + aScenePosition3D.PositionY -= (aSymbolSize.DirectionY/2+1); + eAlignment = LABEL_ALIGN_TOP; + break; + } + + awt::Point aScreenPosition2D;//get the screen position for the labels + sal_Int32 nOffset = 100; //todo maybe calculate this font height dependent + if( nLabelPlacement == css::chart::DataLabelPlacement::OUTSIDE ) + { + PolarPlottingPositionHelper* pPolarPosHelper + = dynamic_cast<PolarPlottingPositionHelper*>(m_pPosHelper); + if( pPolarPosHelper ) + { + PolarLabelPositionHelper aPolarLabelPositionHelper(pPolarPosHelper,m_nDimension,m_xLogicTarget); + aScreenPosition2D = aPolarLabelPositionHelper.getLabelScreenPositionAndAlignmentForLogicValues( + eAlignment, fLogicX, fLogicY, fLogicZ, nOffset ); + } + } + else + { + if(eAlignment==LABEL_ALIGN_CENTER ) + nOffset = 0; + aScreenPosition2D = LabelPositionHelper(m_nDimension,m_xLogicTarget) + .transformSceneToScreenPosition( aScenePosition3D ); + } + + createDataLabel( m_xTextTarget, *pSeries, nIndex + , fLogicValueForLabeDisplay + , aLogicYSumMap[nAttachedAxisIndex], aScreenPosition2D, eAlignment, nOffset ); + } + } + + //remove PointGroupShape if empty + if(!xPointGroupShape_Shapes->getCount()) + xSeriesGroupShape_Shapes->remove(xPointGroupShape_Shapes); + + }//next series in x slot (next y slot) + }//next x slot + }//next z slot + }//next category + + impl_createSeriesShapes(); + +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/NetChart.hxx b/chart2/source/view/charttypes/NetChart.hxx new file mode 100644 index 000000000..a536daf15 --- /dev/null +++ b/chart2/source/view/charttypes/NetChart.hxx @@ -0,0 +1,74 @@ +/* -*- 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 . + */ + +#pragma once + +#include <memory> +#include <VSeriesPlotter.hxx> + +namespace chart +{ + +class NetChart : public VSeriesPlotter +{ + // public methods +public: + NetChart() = delete; + + NetChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bNoArea + , std::unique_ptr<PlottingPositionHelper> pPlottingPositionHelper //takes ownership + ); + virtual ~NetChart() override; + + virtual void createShapes() override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + + // MinimumAndMaximumSupplier + virtual double getMaximumX() override; + virtual bool isExpandIfValuesCloseToBorder( sal_Int32 nDimensionIndex ) override; + virtual bool isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) override; + + virtual LegendSymbolStyle getLegendSymbolStyle() override; + virtual css::uno::Any getExplicitSymbol( const VDataSeries& rSeries, sal_Int32 nPointIndex/*-1 for series symbol*/ ) override; + +private: //methods + void impl_createSeriesShapes(); + bool impl_createArea( VDataSeries* pSeries + , const std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly + , std::vector<std::vector<css::drawing::Position3D>> const * pPreviousSeriesPoly + , PlottingPositionHelper const * pPosHelper ); + bool impl_createLine( VDataSeries* pSeries + , const std::vector<std::vector<css::drawing::Position3D>>* pSeriesPoly + , PlottingPositionHelper const * pPosHelper ); + +private: //member + std::unique_ptr<PlottingPositionHelper> m_pMainPosHelper; + + bool m_bArea;//false -> line or symbol only + bool m_bLine; + + rtl::Reference<SvxShapeGroupAnyD> m_xSeriesTarget; + rtl::Reference<SvxShapeGroupAnyD> m_xTextTarget; +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ 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: */ diff --git a/chart2/source/view/charttypes/PieChart.hxx b/chart2/source/view/charttypes/PieChart.hxx new file mode 100644 index 000000000..9a5b7fb4c --- /dev/null +++ b/chart2/source/view/charttypes/PieChart.hxx @@ -0,0 +1,145 @@ +/* -*- 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 . + */ + +#pragma once + +#include <memory> +#include <VSeriesPlotter.hxx> +#include <basegfx/vector/b2ivector.hxx> +#include <com/sun/star/awt/Point.hpp> + +namespace chart +{ +class PiePositionHelper; + +class PieChart : public VSeriesPlotter +{ + struct ShapeParam; + +public: + PieChart() = delete; + + PieChart( const rtl::Reference< ::chart::ChartType >& xChartTypeModel + , sal_Int32 nDimensionCount, bool bExcludingPositioning ); + virtual ~PieChart() override; + + /** This method creates all shapes needed for representing the pie chart. + */ + virtual void createShapes() override; + virtual void rearrangeLabelToAvoidOverlapIfRequested( const css::awt::Size& rPageSize ) override; + + virtual void setScales( std::vector< ExplicitScaleData >&& rScales, bool bSwapXAndYAxis ) override; + virtual void addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) override; + + virtual css::drawing::Direction3D getPreferredDiagramAspectRatio() const override; + virtual bool shouldSnapRectToUsedArea() override; + + //MinimumAndMaximumSupplier + virtual double getMinimumX() override; + virtual double getMaximumX() override; + virtual double getMinimumYInRange( double fMinimumX, double fMaximumX, sal_Int32 nAxisIndex ) override; + virtual double getMaximumYInRange( double fMinimumX, double fMaximumX, sal_Int32 nAxisIndex ) override; + + virtual bool isExpandBorderToIncrementRhythm( sal_Int32 nDimensionIndex ) override; + virtual bool isExpandIfValuesCloseToBorder( sal_Int32 nDimensionIndex ) override; + virtual bool isExpandWideValuesToZero( sal_Int32 nDimensionIndex ) override; + virtual bool isExpandNarrowValuesTowardZero( sal_Int32 nDimensionIndex ) override; + virtual bool isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) override; + +private: //methods + rtl::Reference<SvxShape> + createDataPoint( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget, + const css::uno::Reference<css::beans::XPropertySet>& xObjectProperties, + const ShapeParam& rParam, + const sal_Int32 nPointCount, + const bool bConcentricExplosion); + + /** This method creates a text shape for a label of a data point. + * + * @param xTextTarget + * where to append the new created text shape. + * @param rSeries + * the data series, the data point belongs to. + * @param nPointIndex + * the index of the data point the label is related to. + * @param rParam + * ShapeParam object. + */ + void createTextLabelShape( + const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget, + VDataSeries& rSeries, sal_Int32 nPointIndex, ShapeParam& rParam ); + + /** This method sets `m_fMaxOffset` to the maximum `Offset` property and + * returns it. There is a `Offset` property for each entry in a data + * series, moreover there exists a shared `Offset` property attached to + * the whole data series. The `Offset` property represents the + * relative distance offset of a slice from the pie center. + * The shared property is used for exploded pie chart, while the property + * attached to single data series entries is used for manual dragging of + * a slice. + * `m_fMaxOffset` is used by `PiePositionHelper::getInnerAndOuterRadius`. + * Note that only the `Offset` properties of the first (x slot) data series + * and its entries are utilized for computing the maximum offset. + */ + double getMaxOffset(); + bool detectLabelOverlapsAndMove(const css::awt::Size& rPageSize);//returns true when there might be more to do + void resetLabelPositionsToPreviousState(); +struct PieLabelInfo; + bool tryMoveLabels( PieLabelInfo const * pFirstBorder, PieLabelInfo const * pSecondBorder + , PieLabelInfo* pCenter, bool bSingleCenter, bool& rbAlternativeMoveDirection + , const css::awt::Size& rPageSize ); + + bool performLabelBestFitInnerPlacement( ShapeParam& rShapeParam + , PieLabelInfo const & rPieLabelInfo ); + +private: //member + std::unique_ptr<PiePositionHelper> + m_pPosHelper; + bool m_bUseRings; + bool m_bSizeExcludesLabelsAndExplodedSegments; + + struct PieLabelInfo + { + PieLabelInfo(); + bool moveAwayFrom( const PieLabelInfo* pFix, const css::awt::Size& rPageSize + , bool bMoveHalfWay, bool bMoveClockwise ); + + rtl::Reference< SvxShapeText > xTextShape; + rtl::Reference< SvxShapeGroupAnyD > xLabelGroupShape; + ::basegfx::B2IVector aFirstPosition; + ::basegfx::B2IVector aOuterPosition; + ::basegfx::B2IVector aOrigin; + double fValue; + bool bMovementAllowed; + bool bMoved; + bool bShowLeaderLine; + rtl::Reference<SvxShapeGroupAnyD> xTextTarget; + PieLabelInfo* pPrevious; + PieLabelInfo* pNext; + css::awt::Point aPreviousPosition; + }; + + std::vector< PieLabelInfo > m_aLabelInfoList; + + double m_fMaxOffset; /// cached max offset value (init'ed to NaN) +}; +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/Splines.cxx b/chart2/source/view/charttypes/Splines.cxx new file mode 100644 index 000000000..2aac96929 --- /dev/null +++ b/chart2/source/view/charttypes/Splines.cxx @@ -0,0 +1,872 @@ +/* -*- 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 "Splines.hxx" +#include <osl/diagnose.h> +#include <com/sun/star/drawing/Position3D.hpp> + +#include <vector> +#include <algorithm> +#include <memory> +#include <cmath> +#include <limits> + +namespace chart +{ +using namespace ::com::sun::star; + +namespace +{ + +typedef std::pair< double, double > tPointType; +typedef std::vector< tPointType > tPointVecType; +typedef tPointVecType::size_type lcl_tSizeType; + +class lcl_SplineCalculation +{ +public: + /** @descr creates an object that calculates cubic splines on construction + + @param rSortedPoints the points for which splines shall be calculated, they need to be sorted in x values + @param fY1FirstDerivation the resulting spline should have the first + derivation equal to this value at the x-value of the first point + of rSortedPoints. If fY1FirstDerivation is set to infinity, a natural + spline is calculated. + @param fYnFirstDerivation the resulting spline should have the first + derivation equal to this value at the x-value of the last point + of rSortedPoints + */ + lcl_SplineCalculation( tPointVecType && rSortedPoints, + double fY1FirstDerivation, + double fYnFirstDerivation ); + + /** @descr creates an object that calculates cubic splines on construction + for the special case of periodic cubic spline + + @param rSortedPoints the points for which splines shall be calculated, + they need to be sorted in x values. First and last y value must be equal + */ + explicit lcl_SplineCalculation( tPointVecType && rSortedPoints); + + /** @descr this function corresponds to the function splint in [1]. + + [1] Numerical Recipes in C, 2nd edition + William H. Press, et al., + Section 3.3, page 116 + */ + double GetInterpolatedValue( double x ); + +private: + /// a copy of the points given in the CTOR + tPointVecType m_aPoints; + + /// the result of the Calculate() method + std::vector< double > m_aSecDerivY; + + double m_fYp1; + double m_fYpN; + + // these values are cached for performance reasons + lcl_tSizeType m_nKLow; + lcl_tSizeType m_nKHigh; + double m_fLastInterpolatedValue; + + /** @descr this function corresponds to the function spline in [1]. + + [1] Numerical Recipes in C, 2nd edition + William H. Press, et al., + Section 3.3, page 115 + */ + void Calculate(); + + /** @descr this function corresponds to the algorithm 4.76 in [2] and + theorem 5.3.7 in [3] + + [2] Engeln-Müllges, Gisela: Numerik-Algorithmen: Verfahren, Beispiele, Anwendungen + Springer, Berlin; Auflage: 9., überarb. und erw. A. (8. Dezember 2004) + Section 4.10.2, page 175 + + [3] Hanrath, Wilhelm: Mathematik III / Numerik, Vorlesungsskript zur + Veranstaltung im WS 2007/2008 + Fachhochschule Aachen, 2009-09-19 + Numerik_01.pdf, downloaded 2011-04-19 via + http://www.fh-aachen.de/index.php?id=11424&no_cache=1&file=5016&uid=44191 + Section 5.3, page 129 + */ + void CalculatePeriodic(); +}; + +lcl_SplineCalculation::lcl_SplineCalculation( + tPointVecType && rSortedPoints, + double fY1FirstDerivation, + double fYnFirstDerivation ) + : m_aPoints( std::move(rSortedPoints) ), + m_fYp1( fY1FirstDerivation ), + m_fYpN( fYnFirstDerivation ), + m_nKLow( 0 ), + m_nKHigh( m_aPoints.size() - 1 ), + m_fLastInterpolatedValue(std::numeric_limits<double>::infinity()) +{ + Calculate(); +} + +lcl_SplineCalculation::lcl_SplineCalculation( + tPointVecType && rSortedPoints) + : m_aPoints( std::move(rSortedPoints) ), + m_fYp1( 0.0 ), /*dummy*/ + m_fYpN( 0.0 ), /*dummy*/ + m_nKLow( 0 ), + m_nKHigh( m_aPoints.size() - 1 ), + m_fLastInterpolatedValue(std::numeric_limits<double>::infinity()) +{ + CalculatePeriodic(); +} + +void lcl_SplineCalculation::Calculate() +{ + if( m_aPoints.size() <= 1 ) + return; + + // n is the last valid index to m_aPoints + const lcl_tSizeType n = m_aPoints.size() - 1; + std::vector< double > u( n ); + m_aSecDerivY.resize( n + 1, 0.0 ); + + if( std::isinf( m_fYp1 ) ) + { + // natural spline + m_aSecDerivY[ 0 ] = 0.0; + u[ 0 ] = 0.0; + } + else + { + m_aSecDerivY[ 0 ] = -0.5; + double xDiff = m_aPoints[ 1 ].first - m_aPoints[ 0 ].first; + u[ 0 ] = ( 3.0 / xDiff ) * + ((( m_aPoints[ 1 ].second - m_aPoints[ 0 ].second ) / xDiff ) - m_fYp1 ); + } + + for( lcl_tSizeType i = 1; i < n; ++i ) + { + tPointType + p_i = m_aPoints[ i ], + p_im1 = m_aPoints[ i - 1 ], + p_ip1 = m_aPoints[ i + 1 ]; + + double sig = ( p_i.first - p_im1.first ) / + ( p_ip1.first - p_im1.first ); + double p = sig * m_aSecDerivY[ i - 1 ] + 2.0; + + m_aSecDerivY[ i ] = ( sig - 1.0 ) / p; + u[ i ] = + ( ( p_ip1.second - p_i.second ) / + ( p_ip1.first - p_i.first ) ) - + ( ( p_i.second - p_im1.second ) / + ( p_i.first - p_im1.first ) ); + u[ i ] = + ( 6.0 * u[ i ] / ( p_ip1.first - p_im1.first ) + - sig * u[ i - 1 ] ) / p; + } + + // initialize to values for natural splines (used for m_fYpN equal to + // infinity) + double qn = 0.0; + double un = 0.0; + + if( ! std::isinf( m_fYpN ) ) + { + qn = 0.5; + double xDiff = m_aPoints[ n ].first - m_aPoints[ n - 1 ].first; + un = ( 3.0 / xDiff ) * + ( m_fYpN - ( m_aPoints[ n ].second - m_aPoints[ n - 1 ].second ) / xDiff ); + } + + m_aSecDerivY[ n ] = ( un - qn * u[ n - 1 ] ) / ( qn * m_aSecDerivY[ n - 1 ] + 1.0 ); + + // note: the algorithm in [1] iterates from n-1 to 0, but as size_type + // may be (usually is) an unsigned type, we can not write k >= 0, as this + // is always true. + for( lcl_tSizeType k = n; k > 0; --k ) + { + m_aSecDerivY[ k - 1 ] = (m_aSecDerivY[ k - 1 ] * m_aSecDerivY[ k ] ) + u[ k - 1 ]; + } +} + +void lcl_SplineCalculation::CalculatePeriodic() +{ + if( m_aPoints.size() <= 1 ) + return; + + // n is the last valid index to m_aPoints + const lcl_tSizeType n = m_aPoints.size() - 1; + + // u is used for vector f in A*c=f in [3], vector a in Ax=a in [2], + // vector z in Rtranspose z = a and Dr=z in [2] + std::vector< double > u( n + 1, 0.0 ); + + // used for vector c in A*c=f and vector x in Ax=a in [2] + m_aSecDerivY.resize( n + 1, 0.0 ); + + // diagonal of matrix A, used index 1 to n + std::vector< double > Adiag( n + 1, 0.0 ); + + // secondary diagonal of matrix A with index 1 to n-1 and upper right element in A[n] + std::vector< double > Aupper( n + 1, 0.0 ); + + // diagonal of matrix D in A=(R transpose)*D*R in [2], used index 1 to n + std::vector< double > Ddiag( n+1, 0.0 ); + + // right column of matrix R, used index 1 to n-2 + std::vector< double > Rright( n-1, 0.0 ); + + // secondary diagonal of matrix R, used index 1 to n-1 + std::vector< double > Rupper( n, 0.0 ); + + if (n<4) + { + if (n==3) + { // special handling of three polynomials, that are four points + double xDiff0 = m_aPoints[ 1 ].first - m_aPoints[ 0 ].first ; + double xDiff1 = m_aPoints[ 2 ].first - m_aPoints[ 1 ].first ; + double xDiff2 = m_aPoints[ 3 ].first - m_aPoints[ 2 ].first ; + double xDiff2p1 = xDiff2 + xDiff1; + double xDiff0p2 = xDiff0 + xDiff2; + double xDiff1p0 = xDiff1 + xDiff0; + double fFactor = 1.5 / (xDiff0*xDiff1 + xDiff1*xDiff2 + xDiff2*xDiff0); + double yDiff0 = (m_aPoints[ 1 ].second - m_aPoints[ 0 ].second) / xDiff0; + double yDiff1 = (m_aPoints[ 2 ].second - m_aPoints[ 1 ].second) / xDiff1; + double yDiff2 = (m_aPoints[ 0 ].second - m_aPoints[ 2 ].second) / xDiff2; + m_aSecDerivY[ 1 ] = fFactor * (yDiff1*xDiff2p1 - yDiff0*xDiff0p2); + m_aSecDerivY[ 2 ] = fFactor * (yDiff2*xDiff0p2 - yDiff1*xDiff1p0); + m_aSecDerivY[ 3 ] = fFactor * (yDiff0*xDiff1p0 - yDiff2*xDiff2p1); + m_aSecDerivY[ 0 ] = m_aSecDerivY[ 3 ]; + } + else if (n==2) + { + // special handling of two polynomials, that are three points + double xDiff0 = m_aPoints[ 1 ].first - m_aPoints[ 0 ].first; + double xDiff1 = m_aPoints[ 2 ].first - m_aPoints[ 1 ].first; + double fHelp = 3.0 * (m_aPoints[ 0 ].second - m_aPoints[ 1 ].second) / (xDiff0*xDiff1); + m_aSecDerivY[ 1 ] = fHelp ; + m_aSecDerivY[ 2 ] = -fHelp ; + m_aSecDerivY[ 0 ] = m_aSecDerivY[ 2 ] ; + } + else + { + // should be handled with natural spline, periodic not possible. + } + } + else + { + double xDiff_i =1.0; // values are dummy; + double xDiff_im1 =1.0; + double yDiff_i = 1.0; + double yDiff_im1 = 1.0; + // fill matrix A and fill right side vector u + for( lcl_tSizeType i=1; i<n; ++i ) + { + xDiff_im1 = m_aPoints[ i ].first - m_aPoints[ i-1 ].first; + xDiff_i = m_aPoints[ i+1 ].first - m_aPoints[ i ].first; + yDiff_im1 = (m_aPoints[ i ].second - m_aPoints[ i-1 ].second) / xDiff_im1; + yDiff_i = (m_aPoints[ i+1 ].second - m_aPoints[ i ].second) / xDiff_i; + Adiag[ i ] = 2 * (xDiff_im1 + xDiff_i); + Aupper[ i ] = xDiff_i; + u [ i ] = 3 * (yDiff_i - yDiff_im1); + } + xDiff_im1 = m_aPoints[ n ].first - m_aPoints[ n-1 ].first; + xDiff_i = m_aPoints[ 1 ].first - m_aPoints[ 0 ].first; + yDiff_im1 = (m_aPoints[ n ].second - m_aPoints[ n-1 ].second) / xDiff_im1; + yDiff_i = (m_aPoints[ 1 ].second - m_aPoints[ 0 ].second) / xDiff_i; + Adiag[ n ] = 2 * (xDiff_im1 + xDiff_i); + Aupper[ n ] = xDiff_i; + u [ n ] = 3 * (yDiff_i - yDiff_im1); + + // decomposite A=(R transpose)*D*R + Ddiag[1] = Adiag[1]; + Rupper[1] = Aupper[1] / Ddiag[1]; + Rright[1] = Aupper[n] / Ddiag[1]; + for( lcl_tSizeType i=2; i<=n-2; ++i ) + { + Ddiag[i] = Adiag[i] - Aupper[ i-1 ] * Rupper[ i-1 ]; + Rupper[ i ] = Aupper[ i ] / Ddiag[ i ]; + Rright[ i ] = - Rright[ i-1 ] * Aupper[ i-1 ] / Ddiag[ i ]; + } + Ddiag[ n-1 ] = Adiag[ n-1 ] - Aupper[ n-2 ] * Rupper[ n-2 ]; + Rupper[ n-1 ] = ( Aupper[ n-1 ] - Aupper[ n-2 ] * Rright[ n-2] ) / Ddiag[ n-1 ]; + double fSum = 0.0; + for ( lcl_tSizeType i=1; i<=n-2; ++i ) + { + fSum += Ddiag[ i ] * Rright[ i ] * Rright[ i ]; + } + Ddiag[ n ] = Adiag[ n ] - fSum - Ddiag[ n-1 ] * Rupper[ n-1 ] * Rupper[ n-1 ]; // bug in [2]! + + // solve forward (R transpose)*z=u, overwrite u with z + for ( lcl_tSizeType i=2; i<=n-1; ++i ) + { + u[ i ] -= u[ i-1 ]* Rupper[ i-1 ]; + } + fSum = 0.0; + for ( lcl_tSizeType i=1; i<=n-2; ++i ) + { + fSum += Rright[ i ] * u[ i ]; + } + u[ n ] = u[ n ] - fSum - Rupper[ n - 1] * u[ n-1 ]; + + // solve forward D*r=z, z is in u, overwrite u with r + for ( lcl_tSizeType i=1; i<=n; ++i ) + { + u[ i ] = u[i] / Ddiag[ i ]; + } + + // solve backward R*x= r, r is in u + m_aSecDerivY[ n ] = u[ n ]; + m_aSecDerivY[ n-1 ] = u[ n-1 ] - Rupper[ n-1 ] * m_aSecDerivY[ n ]; + for ( lcl_tSizeType i=n-2; i>=1; --i) + { + m_aSecDerivY[ i ] = u[ i ] - Rupper[ i ] * m_aSecDerivY[ i+1 ] - Rright[ i ] * m_aSecDerivY[ n ]; + } + // periodic + m_aSecDerivY[ 0 ] = m_aSecDerivY[ n ]; + } + + // adapt m_aSecDerivY for usage in GetInterpolatedValue() + for( lcl_tSizeType i = 0; i <= n ; ++i ) + { + m_aSecDerivY[ i ] *= 2.0; + } + +} + +double lcl_SplineCalculation::GetInterpolatedValue( double x ) +{ + OSL_PRECOND( ( m_aPoints[ 0 ].first <= x ) && + ( x <= m_aPoints[ m_aPoints.size() - 1 ].first ), + "Trying to extrapolate" ); + + const lcl_tSizeType n = m_aPoints.size() - 1; + if( x < m_fLastInterpolatedValue ) + { + m_nKLow = 0; + m_nKHigh = n; + + // calculate m_nKLow and m_nKHigh + // first initialization is done in CTOR + while( m_nKHigh - m_nKLow > 1 ) + { + lcl_tSizeType k = ( m_nKHigh + m_nKLow ) / 2; + if( m_aPoints[ k ].first > x ) + m_nKHigh = k; + else + m_nKLow = k; + } + } + else + { + while( ( m_nKHigh <= n ) && + ( m_aPoints[ m_nKHigh ].first < x ) ) + { + ++m_nKHigh; + ++m_nKLow; + } + OSL_ENSURE( m_nKHigh <= n, "Out of Bounds" ); + } + m_fLastInterpolatedValue = x; + + double h = m_aPoints[ m_nKHigh ].first - m_aPoints[ m_nKLow ].first; + OSL_ENSURE( h != 0, "Bad input to GetInterpolatedValue()" ); + + double a = ( m_aPoints[ m_nKHigh ].first - x ) / h; + double b = ( x - m_aPoints[ m_nKLow ].first ) / h; + + return ( a * m_aPoints[ m_nKLow ].second + + b * m_aPoints[ m_nKHigh ].second + + (( a*a*a - a ) * m_aSecDerivY[ m_nKLow ] + + ( b*b*b - b ) * m_aSecDerivY[ m_nKHigh ] ) * + ( h*h ) / 6.0 ); +} + +// helper methods for B-spline + +// Create parameter t_0 to t_n using the centripetal method with a power of 0.5 +bool createParameterT(const tPointVecType& rUniquePoints, double* t) +{ // precondition: no adjacent identical points + // postcondition: 0 = t_0 < t_1 < ... < t_n = 1 + bool bIsSuccessful = true; + const lcl_tSizeType n = rUniquePoints.size() - 1; + t[0]=0.0; + double fDenominator = 0.0; // initialized for summing up + for (lcl_tSizeType i=1; i<=n ; ++i) + { // 4th root(dx^2+dy^2) + double dx = rUniquePoints[i].first - rUniquePoints[i-1].first; + double dy = rUniquePoints[i].second - rUniquePoints[i-1].second; + if (dx == 0 && dy == 0) + { + bIsSuccessful = false; + break; + } + else + { + fDenominator += sqrt(std::hypot(dx, dy)); + } + } + if (fDenominator == 0.0) + { + bIsSuccessful = false; + } + if (bIsSuccessful) + { + for (lcl_tSizeType j=1; j<=n ; ++j) + { + double fNumerator = 0.0; + for (lcl_tSizeType i=1; i<=j ; ++i) + { + double dx = rUniquePoints[i].first - rUniquePoints[i-1].first; + double dy = rUniquePoints[i].second - rUniquePoints[i-1].second; + fNumerator += sqrt(std::hypot(dx, dy)); + } + t[j] = fNumerator / fDenominator; + + } + // postcondition check + t[n] = 1.0; + double fPrevious = 0.0; + for (lcl_tSizeType i=1; i <= n && bIsSuccessful ; ++i) + { + if (fPrevious >= t[i]) + { + bIsSuccessful = false; + } + else + { + fPrevious = t[i]; + } + } + } + return bIsSuccessful; +} + +void createKnotVector(const lcl_tSizeType n, const sal_uInt32 p, const double* t, double* u) +{ // precondition: 0 = t_0 < t_1 < ... < t_n = 1 + for (lcl_tSizeType j = 0; j <= p; ++j) + { + u[j] = 0.0; + } + for (lcl_tSizeType j = 1; j <= n-p; ++j ) + { + double fSum = 0.0; + for (lcl_tSizeType i = j; i <= j+p-1; ++i) + { + fSum += t[i]; + } + assert(p != 0); + u[j+p] = fSum / p ; + } + for (lcl_tSizeType j = n+1; j <= n+1+p; ++j) + { + u[j] = 1.0; + } +} + +void applyNtoParameterT(const lcl_tSizeType i,const double tk,const sal_uInt32 p,const double* u, double* rowN) +{ + // get N_p(t_k) recursively, only N_(i-p) till N_(i) are relevant, all other N_# are zero + + // initialize with indicator function degree 0 + rowN[p] = 1.0; // all others are zero + + // calculate up to degree p + for (sal_uInt32 s = 1; s <= p; ++s) + { + // first element + double fLeftFactor = 0.0; + double fRightFactor = ( u[i+1] - tk ) / ( u[i+1]- u[i-s+1] ); + // i-s "true index" - (i-p)"shift" = p-s + rowN[p-s] = fRightFactor * rowN[p-s+1]; + + // middle elements + for (sal_uInt32 j = s-1; j>=1 ; --j) + { + fLeftFactor = ( tk - u[i-j] ) / ( u[i-j+s] - u[i-j] ) ; + fRightFactor = ( u[i-j+s+1] - tk ) / ( u[i-j+s+1] - u[i-j+1] ); + // i-j "true index" - (i-p)"shift" = p-j + rowN[p-j] = fLeftFactor * rowN[p-j] + fRightFactor * rowN[p-j+1]; + } + + // last element + fLeftFactor = ( tk - u[i] ) / ( u[i+s] - u[i] ); + // i "true index" - (i-p)"shift" = p + rowN[p] = fLeftFactor * rowN[p]; + } +} + +} // anonymous namespace + +// Calculates uniform parametric splines with subinterval length 1, +// according ODF1.2 part 1, chapter 'chart interpolation'. +void SplineCalculater::CalculateCubicSplines( + const std::vector<std::vector<css::drawing::Position3D>>& rInput + , std::vector<std::vector<css::drawing::Position3D>>& rResult + , sal_uInt32 nGranularity ) +{ + OSL_PRECOND( nGranularity > 0, "Granularity is invalid" ); + + sal_uInt32 nOuterCount = rInput.size(); + + rResult.resize(nOuterCount); + + auto pSequence = rResult.data(); + + if( !nOuterCount ) + return; + + for( sal_uInt32 nOuter = 0; nOuter < nOuterCount; ++nOuter ) + { + if( rInput[nOuter].size() <= 1 ) + continue; //we need at least two points + + sal_uInt32 nMaxIndexPoints = rInput[nOuter].size()-1; // is >=1 + const css::drawing::Position3D* pOld = rInput[nOuter].data(); + + std::vector < double > aParameter(nMaxIndexPoints+1); + aParameter[0]=0.0; + for( sal_uInt32 nIndex=1; nIndex<=nMaxIndexPoints; nIndex++ ) + { + aParameter[nIndex]=aParameter[nIndex-1]+1; + } + + // Split the calculation to X, Y and Z coordinate + tPointVecType aInputX; + aInputX.resize(nMaxIndexPoints+1); + tPointVecType aInputY; + aInputY.resize(nMaxIndexPoints+1); + tPointVecType aInputZ; + aInputZ.resize(nMaxIndexPoints+1); + for (sal_uInt32 nN=0;nN<=nMaxIndexPoints; nN++ ) + { + aInputX[ nN ].first=aParameter[nN]; + aInputX[ nN ].second=pOld[ nN ].PositionX; + aInputY[ nN ].first=aParameter[nN]; + aInputY[ nN ].second=pOld[ nN ].PositionY; + aInputZ[ nN ].first=aParameter[nN]; + aInputZ[ nN ].second=pOld[ nN ].PositionZ; + } + + // generate a spline for each coordinate. It holds the complete + // information to calculate each point of the curve + std::unique_ptr<lcl_SplineCalculation> aSplineX; + std::unique_ptr<lcl_SplineCalculation> aSplineY; + // lcl_SplineCalculation* aSplineZ; the z-coordinates of all points in + // a data series are equal. No spline calculation needed, but copy + // coordinate to output + + if( pOld[ 0 ].PositionX == pOld[nMaxIndexPoints].PositionX && + pOld[ 0 ].PositionY == pOld[nMaxIndexPoints].PositionY && + pOld[ 0 ].PositionZ == pOld[nMaxIndexPoints].PositionZ && + nMaxIndexPoints >=2 ) + { // periodic spline + aSplineX = std::make_unique<lcl_SplineCalculation>(std::move(aInputX)); + aSplineY = std::make_unique<lcl_SplineCalculation>(std::move(aInputY)); + } + else // generate the kind "natural spline" + { + double fXDerivation = std::numeric_limits<double>::infinity(); + double fYDerivation = std::numeric_limits<double>::infinity(); + aSplineX = std::make_unique<lcl_SplineCalculation>(std::move(aInputX), fXDerivation, fXDerivation); + aSplineY = std::make_unique<lcl_SplineCalculation>(std::move(aInputY), fYDerivation, fYDerivation); + } + + // fill result polygon with calculated values + pSequence[nOuter].resize( nMaxIndexPoints*nGranularity + 1); + + css::drawing::Position3D* pNew = pSequence[nOuter].data(); + + sal_uInt32 nNewPointIndex = 0; // Index in result points + + for( sal_uInt32 ni = 0; ni < nMaxIndexPoints; ni++ ) + { + // given point is surely a curve point + pNew[nNewPointIndex].PositionX = pOld[ni].PositionX; + pNew[nNewPointIndex].PositionY = pOld[ni].PositionY; + pNew[nNewPointIndex].PositionZ = pOld[ni].PositionZ; + nNewPointIndex++; + + // calculate intermediate points + double fInc = ( aParameter[ ni+1 ] - aParameter[ni] ) / static_cast< double >( nGranularity ); + for(sal_uInt32 nj = 1; nj < nGranularity; nj++) + { + double fParam = aParameter[ni] + ( fInc * static_cast< double >( nj ) ); + + pNew[nNewPointIndex].PositionX = aSplineX->GetInterpolatedValue( fParam ); + pNew[nNewPointIndex].PositionY = aSplineY->GetInterpolatedValue( fParam ); + // pNewZ[nNewPointIndex]=aSplineZ->GetInterpolatedValue( fParam ); + pNew[nNewPointIndex].PositionZ = pOld[ni].PositionZ; + nNewPointIndex++; + } + } + // add last point + pNew[nNewPointIndex] = pOld[nMaxIndexPoints]; + } +} + +void SplineCalculater::CalculateBSplines( + const std::vector<std::vector<css::drawing::Position3D>>& rInput + , std::vector<std::vector<css::drawing::Position3D>>& rResult + , sal_uInt32 nResolution + , sal_uInt32 nDegree ) +{ + // nResolution is ODF1.2 file format attribute chart:spline-resolution and + // ODF1.2 spec variable k. Causion, k is used as index in the spec in addition. + // nDegree is ODF1.2 file format attribute chart:spline-order and + // ODF1.2 spec variable p + OSL_ASSERT( nResolution > 1 ); + OSL_ASSERT( nDegree >= 1 ); + + // limit the b-spline degree at 15 to prevent insanely large sets of points + sal_uInt32 p = std::min<sal_uInt32>(nDegree, 15); + + sal_Int32 nOuterCount = rInput.size(); + + rResult.resize(nOuterCount); + + auto pSequence = rResult.data(); + + if( !nOuterCount ) + return; // no input + + for( sal_Int32 nOuter = 0; nOuter < nOuterCount; ++nOuter ) + { + if( rInput[nOuter].size() <= 1 ) + continue; // need at least 2 points, next piece of the series + + // Copy input to vector of points and remove adjacent double points. The + // Z-coordinate is equal for all points in a series and holds the depth + // in 3D mode, simple copying is enough. + lcl_tSizeType nMaxIndexPoints = rInput[nOuter].size()-1; // is >=1 + const css::drawing::Position3D* pOld = rInput[nOuter].data(); + double fZCoordinate = pOld[0].PositionZ; + tPointVecType aPointsIn; + aPointsIn.resize(nMaxIndexPoints+1); + for (lcl_tSizeType i = 0; i <= nMaxIndexPoints; ++i ) + { + aPointsIn[ i ].first = pOld[i].PositionX; + aPointsIn[ i ].second = pOld[i].PositionY; + } + aPointsIn.erase( std::unique( aPointsIn.begin(), aPointsIn.end()), + aPointsIn.end() ); + + // n is the last valid index to the reduced aPointsIn + // There are n+1 valid data points. + const lcl_tSizeType n = aPointsIn.size() - 1; + if (n < 1 || p > n) + continue; // need at least 2 points, degree p needs at least n+1 points + // next piece of series + + std::vector<double> t(n + 1); + if (!createParameterT(aPointsIn, t.data())) + { + continue; // next piece of series + } + + lcl_tSizeType m = n + p + 1; + std::vector<double> u(m + 1); + createKnotVector(n, p, t.data(), u.data()); + + // The matrix N contains the B-spline basis functions applied to parameters. + // In each row only p+1 adjacent elements are non-zero. The starting + // column in a higher row is equal or greater than in the lower row. + // To store this matrix the non-zero elements are shifted to column 0 + // and the amount of shifting is remembered in an array. + std::vector<std::vector<double>> aMatN(n + 1, std::vector<double>(p + 1)); + std::vector<lcl_tSizeType> aShift(n + 1); + aMatN[0][0] = 1.0; //all others are zero + aShift[0] = 0; + aMatN[n][0] = 1.0; + aShift[n] = n; + for (lcl_tSizeType k = 1; k<=n-1; ++k) + { // all basis functions are applied to t_k, + // results are elements in row k in matrix N + + // find the one interval with u_i <= t_k < u_(i+1) + // remember u_0 = ... = u_p = 0.0 and u_(m-p) = ... u_m = 1.0 and 0<t_k<1 + lcl_tSizeType i = p; + while (u[i] > t[k] || t[k] >= u[i+1]) + { + ++i; + } + + // index in reduced matrix aMatN = (index in full matrix N) - (i-p) + aShift[k] = i - p; + + applyNtoParameterT(i, t[k], p, u.data(), aMatN[k].data()); + } // next row k + + // Get matrix C of control points from the matrix equation aMatN * C = aPointsIn + // aPointsIn is overwritten with C. + // Gaussian elimination is possible without pivoting, see reference + lcl_tSizeType r = 0; // true row index + lcl_tSizeType c = 0; // true column index + double fDivisor = 1.0; // used for diagonal element + double fEliminate = 1.0; // used for the element, that will become zero + bool bIsSuccessful = true; + for (c = 0 ; c <= n && bIsSuccessful; ++c) + { + // search for first non-zero downwards + r = c; + while ( r < n && aMatN[r][c-aShift[r]] == 0 ) + { + ++r; + } + if (aMatN[r][c-aShift[r]] == 0.0) + { + // Matrix N is singular, although this is mathematically impossible + bIsSuccessful = false; + } + else + { + // exchange total row r with total row c if necessary + if (r != c) + { + std::swap( aMatN[r], aMatN[c] ); + std::swap( aPointsIn[r], aPointsIn[c] ); + std::swap( aShift[r], aShift[c] ); + } + + // divide row c, so that element(c,c) becomes 1 + fDivisor = aMatN[c][c-aShift[c]]; // not zero, see above + for (sal_uInt32 i = 0; i <= p; ++i) + { + aMatN[c][i] /= fDivisor; + } + aPointsIn[c].first /= fDivisor; + aPointsIn[c].second /= fDivisor; + + // eliminate forward, examine row c+1 to n-1 (worst case) + // stop if first non-zero element in row has an higher column as c + // look at nShift for that, elements in nShift are equal or increasing + for ( r = c+1; r < n && aShift[r]<=c ; ++r) + { + fEliminate = aMatN[r][0]; + if (fEliminate != 0.0) // else accidentally zero, nothing to do + { + for (sal_uInt32 i = 1; i <= p; ++i) + { + aMatN[r][i-1] = aMatN[r][i] - fEliminate * aMatN[c][i]; + } + aMatN[r][p]=0; + aPointsIn[r].first -= fEliminate * aPointsIn[c].first; + aPointsIn[r].second -= fEliminate * aPointsIn[c].second; + ++aShift[r]; + } + } + } + }// upper triangle form is reached + if( bIsSuccessful) + { + // eliminate backwards, begin with last column + for (lcl_tSizeType cc = n; cc >= 1; --cc ) + { + // In row cc the diagonal element(cc,cc) == 1 and all elements left from + // diagonal are zero and do not influence other rows. + // Full matrix N has semibandwidth < p, therefore element(r,c) is + // zero, if abs(r-cc)>=p. abs(r-cc)=cc-r, because r<cc. + r = cc - 1; + while ( r !=0 && cc-r < p ) + { + fEliminate = aMatN[r][ cc - aShift[r] ]; + if ( fEliminate != 0.0) // else element is accidentally zero, no action needed + { + // row r -= fEliminate * row cc only relevant for right side + aMatN[r][cc - aShift[r]] = 0.0; + aPointsIn[r].first -= fEliminate * aPointsIn[cc].first; + aPointsIn[r].second -= fEliminate * aPointsIn[cc].second; + } + --r; + } + } + // aPointsIn contains the control points now. + + // calculate the intermediate points according given resolution + // using deBoor-Cox algorithm + lcl_tSizeType nNewSize = nResolution * n + 1; + pSequence[nOuter].resize(nNewSize); + css::drawing::Position3D* pNew = pSequence[nOuter].data(); + pNew[0].PositionX = aPointsIn[0].first; + pNew[0].PositionY = aPointsIn[0].second; + pNew[0].PositionZ = fZCoordinate; // Precondition: z-coordinates of all points of a series are equal + pNew[nNewSize -1 ].PositionX = aPointsIn[n].first; + pNew[nNewSize -1 ].PositionY = aPointsIn[n].second; + pNew[nNewSize -1 ].PositionZ = fZCoordinate; + std::vector<double> aP(m + 1); + lcl_tSizeType nLow = 0; + for ( lcl_tSizeType nTIndex = 0; nTIndex <= n-1; ++nTIndex) + { + for (sal_uInt32 nResolutionStep = 1; + nResolutionStep <= nResolution && ( nTIndex != n-1 || nResolutionStep != nResolution); + ++nResolutionStep) + { + lcl_tSizeType nNewIndex = nTIndex * nResolution + nResolutionStep; + double ux = t[nTIndex] + nResolutionStep * ( t[nTIndex+1] - t[nTIndex]) /nResolution; + + // get index nLow, so that u[nLow]<= ux < u[nLow +1] + // continue from previous nLow + while ( u[nLow] <= ux) + { + ++nLow; + } + --nLow; + + // x-coordinate + for (lcl_tSizeType i = nLow-p; i <= nLow; ++i) + { + aP[i] = aPointsIn[i].first; + } + for (sal_uInt32 lcl_Degree = 1; lcl_Degree <= p; ++lcl_Degree) + { + for (lcl_tSizeType i = nLow; i >= nLow + lcl_Degree - p; --i) + { + double fFactor = ( ux - u[i] ) / ( u[i+p+1-lcl_Degree] - u[i]); + aP[i] = (1 - fFactor)* aP[i-1] + fFactor * aP[i]; + } + } + pNew[nNewIndex].PositionX = aP[nLow]; + + // y-coordinate + for (lcl_tSizeType i = nLow - p; i <= nLow; ++i) + { + aP[i] = aPointsIn[i].second; + } + for (sal_uInt32 lcl_Degree = 1; lcl_Degree <= p; ++lcl_Degree) + { + for (lcl_tSizeType i = nLow; i >= nLow +lcl_Degree - p; --i) + { + double fFactor = ( ux - u[i] ) / ( u[i+p+1-lcl_Degree] - u[i]); + aP[i] = (1 - fFactor)* aP[i-1] + fFactor * aP[i]; + } + } + pNew[nNewIndex].PositionY = aP[nLow]; + pNew[nNewIndex].PositionZ = fZCoordinate; + } + } + } + } // next piece of the series +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/Splines.hxx b/chart2/source/view/charttypes/Splines.hxx new file mode 100644 index 000000000..b83c13931 --- /dev/null +++ b/chart2/source/view/charttypes/Splines.hxx @@ -0,0 +1,48 @@ +/* -*- 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 . + */ + +#pragma once + +#include <sal/types.h> +#include <vector> + +namespace com::sun::star::drawing { struct PolyPolygonShape3D; } +namespace com::sun::star::drawing { struct Position3D; } + +namespace chart +{ + +class SplineCalculater +{ +public: + static void CalculateCubicSplines( + const std::vector<std::vector<css::drawing::Position3D>>& rPoints + , std::vector<std::vector<css::drawing::Position3D>>& rResult + , sal_uInt32 nGranularity ); + + static void CalculateBSplines( + const std::vector<std::vector<css::drawing::Position3D>>& rPoints + , std::vector<std::vector<css::drawing::Position3D>>& rResult + , sal_uInt32 nGranularity + , sal_uInt32 nSplineDepth ); +}; + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/chart2/source/view/charttypes/VSeriesPlotter.cxx b/chart2/source/view/charttypes/VSeriesPlotter.cxx new file mode 100644 index 000000000..fd3a1fe1f --- /dev/null +++ b/chart2/source/view/charttypes/VSeriesPlotter.cxx @@ -0,0 +1,2847 @@ +/* -*- 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 <cstddef> +#include <limits> +#include <memory> +#include <VSeriesPlotter.hxx> +#include <BaseGFXHelper.hxx> +#include <VLineProperties.hxx> +#include <ShapeFactory.hxx> +#include <Diagram.hxx> +#include <BaseCoordinateSystem.hxx> +#include <DataSeries.hxx> + +#include <CommonConverters.hxx> +#include <ExplicitCategoriesProvider.hxx> +#include <ObjectIdentifier.hxx> +#include <StatisticsHelper.hxx> +#include <PlottingPositionHelper.hxx> +#include <LabelPositionHelper.hxx> +#include <ChartType.hxx> +#include <ChartTypeHelper.hxx> +#include <Clipping.hxx> +#include <servicenames_charttypes.hxx> +#include <NumberFormatterWrapper.hxx> +#include <DataSeriesHelper.hxx> +#include <RegressionCurveModel.hxx> +#include <RegressionCurveHelper.hxx> +#include <VLegendSymbolFactory.hxx> +#include <FormattedStringHelper.hxx> +#include <RelativePositionHelper.hxx> +#include <DateHelper.hxx> +#include <DiagramHelper.hxx> +#include <defines.hxx> +#include <ChartModel.hxx> + +//only for creation: @todo remove if all plotter are uno components and instantiated via servicefactory +#include "BarChart.hxx" +#include "PieChart.hxx" +#include "AreaChart.hxx" +#include "CandleStickChart.hxx" +#include "BubbleChart.hxx" +#include "NetChart.hxx" +#include <unonames.hxx> +#include <SpecialCharacters.hxx> + +#include <com/sun/star/chart2/DataPointLabel.hpp> +#include <com/sun/star/chart/ErrorBarStyle.hpp> +#include <com/sun/star/chart/TimeUnit.hpp> +#include <com/sun/star/chart2/MovingAverageType.hpp> +#include <com/sun/star/chart2/XDataPointCustomLabelField.hpp> +#include <com/sun/star/container/XChild.hpp> +#include <com/sun/star/chart2/RelativePosition.hpp> +#include <o3tl/safeint.hxx> +#include <tools/color.hxx> +#include <tools/UnitConversion.hxx> +#include <rtl/ustrbuf.hxx> +#include <rtl/math.hxx> +#include <basegfx/vector/b2dvector.hxx> +#include <com/sun/star/drawing/LineStyle.hpp> +#include <com/sun/star/util/XCloneable.hpp> + +#include <unotools/localedatawrapper.hxx> +#include <comphelper/sequence.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <tools/diagnose_ex.h> +#include <sal/log.hxx> + +#include <functional> +#include <map> +#include <unordered_map> + + +namespace chart { + +using namespace ::com::sun::star; +using namespace ::com::sun::star::chart; +using namespace ::com::sun::star::chart2; +using ::com::sun::star::uno::Reference; +using ::com::sun::star::uno::Sequence; + +VDataSeriesGroup::CachedYValues::CachedYValues() + : m_bValuesDirty(true) + , m_fMinimumY(0.0) + , m_fMaximumY(0.0) +{ +} + +VDataSeriesGroup::VDataSeriesGroup( std::unique_ptr<VDataSeries> pSeries ) + : m_aSeriesVector(1) + , m_bMaxPointCountDirty(true) + , m_nMaxPointCount(0) +{ + m_aSeriesVector[0] = std::move(pSeries); +} + +VDataSeriesGroup::VDataSeriesGroup(VDataSeriesGroup&& other) noexcept + : m_aSeriesVector( std::move(other.m_aSeriesVector) ) + , m_bMaxPointCountDirty( other.m_bMaxPointCountDirty ) + , m_nMaxPointCount( other.m_nMaxPointCount ) + , m_aListOfCachedYValues( std::move(other.m_aListOfCachedYValues) ) +{ +} + +VDataSeriesGroup::~VDataSeriesGroup() +{ +} + +void VDataSeriesGroup::deleteSeries() +{ + //delete all data series help objects: + m_aSeriesVector.clear(); +} + +void VDataSeriesGroup::addSeries( std::unique_ptr<VDataSeries> pSeries ) +{ + m_aSeriesVector.push_back(std::move(pSeries)); + m_bMaxPointCountDirty=true; +} + +sal_Int32 VDataSeriesGroup::getSeriesCount() const +{ + return m_aSeriesVector.size(); +} + +VSeriesPlotter::VSeriesPlotter( const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount, bool bCategoryXAxis ) + : PlotterBase( nDimensionCount ) + , m_pMainPosHelper( nullptr ) + , m_xChartTypeModel(xChartTypeModel) + , m_bCategoryXAxis(bCategoryXAxis) + , m_nTimeResolution(css::chart::TimeUnit::DAY) + , m_aNullDate(30,12,1899) + , m_pExplicitCategoriesProvider(nullptr) + , m_bPointsWereSkipped(false) + , m_bPieLabelsAllowToMove(false) + , m_aAvailableOuterRect(0, 0, 0, 0) +{ + SAL_WARN_IF(!m_xChartTypeModel.is(),"chart2","no XChartType available in view, fallback to default values may be wrong"); +} + +VSeriesPlotter::~VSeriesPlotter() +{ + //delete all data series help objects: + for (std::vector<VDataSeriesGroup> & rGroupVector : m_aZSlots) + { + for (VDataSeriesGroup & rGroup : rGroupVector) + { + rGroup.deleteSeries(); + } + rGroupVector.clear(); + } + m_aZSlots.clear(); + + m_aSecondaryPosHelperMap.clear(); + + m_aSecondaryValueScales.clear(); +} + +void VSeriesPlotter::addSeries( std::unique_ptr<VDataSeries> pSeries, sal_Int32 zSlot, sal_Int32 xSlot, sal_Int32 ySlot ) +{ + //take ownership of pSeries + + OSL_PRECOND( pSeries, "series to add is NULL" ); + if(!pSeries) + return; + + if(m_bCategoryXAxis) + { + if( m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis() ) + pSeries->setXValues( m_pExplicitCategoriesProvider->getOriginalCategories() ); + else + pSeries->setCategoryXAxis(); + } + else + { + if( m_pExplicitCategoriesProvider ) + pSeries->setXValuesIfNone( m_pExplicitCategoriesProvider->getOriginalCategories() ); + } + + if(zSlot<0 || o3tl::make_unsigned(zSlot)>=m_aZSlots.size()) + { + //new z slot + std::vector< VDataSeriesGroup > aZSlot; + aZSlot.emplace_back( std::move(pSeries) ); + m_aZSlots.push_back( std::move(aZSlot) ); + } + else + { + //existing zslot + std::vector< VDataSeriesGroup >& rXSlots = m_aZSlots[zSlot]; + + if(xSlot<0 || o3tl::make_unsigned(xSlot)>=rXSlots.size()) + { + //append the series to already existing x series + rXSlots.emplace_back( std::move(pSeries) ); + } + else + { + //x slot is already occupied + //y slot decides what to do: + + VDataSeriesGroup& rYSlots = rXSlots[xSlot]; + sal_Int32 nYSlotCount = rYSlots.getSeriesCount(); + + if( ySlot < -1 ) + { + //move all existing series in the xSlot to next slot + //@todo + OSL_FAIL( "Not implemented yet"); + } + else if( ySlot == -1 || ySlot >= nYSlotCount) + { + //append the series to already existing y series + rYSlots.addSeries( std::move(pSeries) ); + } + else + { + //y slot is already occupied + //insert at given y and x position + + //@todo + OSL_FAIL( "Not implemented yet"); + } + } + } +} + +drawing::Direction3D VSeriesPlotter::getPreferredDiagramAspectRatio() const +{ + drawing::Direction3D aRet(1.0,1.0,1.0); + if (!m_pPosHelper) + return aRet; + + drawing::Direction3D aScale( m_pPosHelper->getScaledLogicWidth() ); + aRet.DirectionZ = aScale.DirectionZ*0.2; + if(aRet.DirectionZ>1.0) + aRet.DirectionZ=1.0; + if(aRet.DirectionZ>10) + aRet.DirectionZ=10; + return aRet; +} + +void VSeriesPlotter::releaseShapes() +{ + for (std::vector<VDataSeriesGroup> const & rGroupVector : m_aZSlots) + { + for (VDataSeriesGroup const & rGroup : rGroupVector) + { + //iterate through all series in this x slot + for (std::unique_ptr<VDataSeries> const & pSeries : rGroup.m_aSeriesVector) + { + pSeries->releaseShapes(); + } + } + } +} + +rtl::Reference<SvxShapeGroupAnyD> VSeriesPlotter::getSeriesGroupShape( VDataSeries* pDataSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + if( !pDataSeries->m_xGroupShape ) + //create a group shape for this series and add to logic target: + pDataSeries->m_xGroupShape = createGroupShape( xTarget,pDataSeries->getCID() ); + return pDataSeries->m_xGroupShape; +} + +rtl::Reference<SvxShapeGroupAnyD> VSeriesPlotter::getSeriesGroupShapeFrontChild( VDataSeries* pDataSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + if(!pDataSeries->m_xFrontSubGroupShape) + { + //ensure that the series group shape is already created + rtl::Reference<SvxShapeGroupAnyD> xSeriesShapes( getSeriesGroupShape( pDataSeries, xTarget ) ); + //ensure that the back child is created first + getSeriesGroupShapeBackChild( pDataSeries, xTarget ); + //use series group shape as parent for the new created front group shape + pDataSeries->m_xFrontSubGroupShape = createGroupShape( xSeriesShapes ); + } + return pDataSeries->m_xFrontSubGroupShape; +} + +rtl::Reference<SvxShapeGroupAnyD> VSeriesPlotter::getSeriesGroupShapeBackChild( VDataSeries* pDataSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + if(!pDataSeries->m_xBackSubGroupShape) + { + //ensure that the series group shape is already created + rtl::Reference<SvxShapeGroupAnyD> xSeriesShapes( getSeriesGroupShape( pDataSeries, xTarget ) ); + //use series group shape as parent for the new created back group shape + pDataSeries->m_xBackSubGroupShape = createGroupShape( xSeriesShapes ); + } + return pDataSeries->m_xBackSubGroupShape; +} + +rtl::Reference<SvxShapeGroup> VSeriesPlotter::getLabelsGroupShape( VDataSeries& rDataSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTextTarget ) +{ + //xTextTarget needs to be a 2D shape container always! + if(!rDataSeries.m_xLabelsGroupShape) + { + //create a 2D group shape for texts of this series and add to text target: + rDataSeries.m_xLabelsGroupShape = ShapeFactory::createGroup2D( xTextTarget, rDataSeries.getLabelsCID() ); + } + return rDataSeries.m_xLabelsGroupShape; +} + +rtl::Reference<SvxShapeGroupAnyD> VSeriesPlotter::getErrorBarsGroupShape( VDataSeries& rDataSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , bool bYError ) +{ + rtl::Reference<SvxShapeGroupAnyD> &rShapeGroup = + bYError ? rDataSeries.m_xErrorYBarsGroupShape : rDataSeries.m_xErrorXBarsGroupShape; + + if(!rShapeGroup) + { + //create a group shape for this series and add to logic target: + rShapeGroup = createGroupShape( xTarget,rDataSeries.getErrorBarsCID(bYError) ); + } + return rShapeGroup; + +} + +OUString VSeriesPlotter::getLabelTextForValue( VDataSeries const & rDataSeries + , sal_Int32 nPointIndex + , double fValue + , bool bAsPercentage ) +{ + OUString aNumber; + + if (m_apNumberFormatterWrapper) + { + sal_Int32 nNumberFormatKey = 0; + if( rDataSeries.hasExplicitNumberFormat(nPointIndex,bAsPercentage) ) + nNumberFormatKey = rDataSeries.getExplicitNumberFormat(nPointIndex,bAsPercentage); + else if( bAsPercentage ) + { + sal_Int32 nPercentFormat = DiagramHelper::getPercentNumberFormat( m_apNumberFormatterWrapper->getNumberFormatsSupplier() ); + if( nPercentFormat != -1 ) + nNumberFormatKey = nPercentFormat; + } + else + { + nNumberFormatKey = rDataSeries.detectNumberFormatKey( nPointIndex ); + } + if(nNumberFormatKey<0) + nNumberFormatKey=0; + + Color nLabelCol; + bool bColChanged; + aNumber = m_apNumberFormatterWrapper->getFormattedString( + nNumberFormatKey, fValue, nLabelCol, bColChanged ); + //@todo: change color of label if bColChanged is true + } + else + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + const OUString& aNumDecimalSep = rLocaleDataWrapper.getNumDecimalSep(); + assert(aNumDecimalSep.getLength() > 0); + sal_Unicode cDecSeparator = aNumDecimalSep[0]; + aNumber = ::rtl::math::doubleToUString( fValue, rtl_math_StringFormat_G /*rtl_math_StringFormat*/ + , 3/*DecPlaces*/ , cDecSeparator ); + } + return aNumber; +} + +rtl::Reference<SvxShapeText> VSeriesPlotter::createDataLabel( const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , VDataSeries& rDataSeries + , sal_Int32 nPointIndex + , double fValue + , double fSumValue + , const awt::Point& rScreenPosition2D + , LabelAlignment eAlignment + , sal_Int32 nOffset + , sal_Int32 nTextWidth ) +{ + rtl::Reference<SvxShapeText> xTextShape; + Sequence<uno::Reference<XDataPointCustomLabelField>> aCustomLabels; + + try + { + const uno::Reference< css::beans::XPropertySet >& xPropertySet( + rDataSeries.getPropertiesOfPoint( nPointIndex ) ); + if( xPropertySet.is() ) + { + uno::Any aAny = xPropertySet->getPropertyValue( CHART_UNONAME_CUSTOM_LABEL_FIELDS ); + if( aAny.hasValue() ) + { + aAny >>= aCustomLabels; + } + } + + awt::Point aScreenPosition2D(rScreenPosition2D); + if(eAlignment==LABEL_ALIGN_LEFT) + aScreenPosition2D.X -= nOffset; + else if(eAlignment==LABEL_ALIGN_RIGHT) + aScreenPosition2D.X += nOffset; + else if(eAlignment==LABEL_ALIGN_TOP) + aScreenPosition2D.Y -= nOffset; + else if(eAlignment==LABEL_ALIGN_BOTTOM) + aScreenPosition2D.Y += nOffset; + + rtl::Reference<SvxShapeGroup> xTarget_ = + ShapeFactory::createGroup2D( + getLabelsGroupShape(rDataSeries, xTarget), + ObjectIdentifier::createPointCID( rDataSeries.getLabelCID_Stub(), nPointIndex)); + + //check whether the label needs to be created and how: + DataPointLabel* pLabel = rDataSeries.getDataPointLabelIfLabel( nPointIndex ); + + if( !pLabel ) + return xTextShape; + + //prepare legend symbol + + // get the font size for the label through the "CharHeight" property + // attached to the passed data series entry. + // (By tracing font height values it results that for pie chart the + // font size is not the same for all labels, but here no font size + // modification occurs). + float fViewFontSize( 10.0 ); + { + uno::Reference< beans::XPropertySet > xProps( rDataSeries.getPropertiesOfPoint( nPointIndex ) ); + if( xProps.is() ) + xProps->getPropertyValue( "CharHeight") >>= fViewFontSize; + fViewFontSize = convertPointToMm100(fViewFontSize); + } + + // the font height is used for computing the size of an optional legend + // symbol to be prepended to the text label. + rtl::Reference< SvxShapeGroup > xSymbol; + if(pLabel->ShowLegendSymbol) + { + sal_Int32 nSymbolHeight = static_cast< sal_Int32 >( fViewFontSize * 0.6 ); + awt::Size aCurrentRatio = getPreferredLegendKeyAspectRatio(); + sal_Int32 nSymbolWidth = aCurrentRatio.Width; + if( aCurrentRatio.Height > 0 ) + { + nSymbolWidth = nSymbolHeight* aCurrentRatio.Width/aCurrentRatio.Height; + } + awt::Size aMaxSymbolExtent( nSymbolWidth, nSymbolHeight ); + + if( rDataSeries.isVaryColorsByPoint() ) + xSymbol = VSeriesPlotter::createLegendSymbolForPoint( aMaxSymbolExtent, rDataSeries, nPointIndex, xTarget_ ); + else + xSymbol = VSeriesPlotter::createLegendSymbolForSeries( aMaxSymbolExtent, rDataSeries, xTarget_ ); + } + + //prepare text + bool bTextWrap = false; + OUString aSeparator(" "); + double fRotationDegrees = 0.0; + try + { + uno::Reference< beans::XPropertySet > xPointProps( rDataSeries.getPropertiesOfPoint( nPointIndex ) ); + if(xPointProps.is()) + { + xPointProps->getPropertyValue( "TextWordWrap" ) >>= bTextWrap; + xPointProps->getPropertyValue( "LabelSeparator" ) >>= aSeparator; + // Extract the optional text rotation through the + // "TextRotation" property attached to the passed data point. + xPointProps->getPropertyValue( "TextRotation" ) >>= fRotationDegrees; + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + sal_Int32 nLineCountForSymbolsize = 0; + sal_uInt32 nTextListLength = 4; + sal_uInt32 nCustomLabelsCount = aCustomLabels.getLength(); + Sequence< OUString > aTextList( nTextListLength ); + + bool bUseCustomLabel = nCustomLabelsCount > 0; + if( bUseCustomLabel ) + { + nTextListLength = ( nCustomLabelsCount > 3 ) ? nCustomLabelsCount : 3; + aSeparator = ""; + aTextList = Sequence< OUString >( nTextListLength ); + auto pTextList = aTextList.getArray(); + for( sal_uInt32 i = 0; i < nCustomLabelsCount; ++i ) + { + switch( aCustomLabels[i]->getFieldType() ) + { + case DataPointCustomLabelFieldType_VALUE: + { + pTextList[i] = getLabelTextForValue( rDataSeries, nPointIndex, fValue, false ); + break; + } + case DataPointCustomLabelFieldType_CATEGORYNAME: + { + pTextList[i] = getCategoryName( nPointIndex ); + break; + } + case DataPointCustomLabelFieldType_SERIESNAME: + { + OUString aRole; + if ( m_xChartTypeModel ) + aRole = m_xChartTypeModel->getRoleOfSequenceForSeriesLabel(); + const rtl::Reference< DataSeries >& xSeries( rDataSeries.getModel() ); + pTextList[i] = DataSeriesHelper::getDataSeriesLabel( xSeries, aRole ); + break; + } + case DataPointCustomLabelFieldType_PERCENTAGE: + { + if(fSumValue == 0.0) + fSumValue = 1.0; + fValue /= fSumValue; + if(fValue < 0) + fValue *= -1.0; + + pTextList[i] = getLabelTextForValue(rDataSeries, nPointIndex, fValue, true); + break; + } + case DataPointCustomLabelFieldType_CELLRANGE: + { + if (aCustomLabels[i]->getDataLabelsRange()) + pTextList[i] = aCustomLabels[i]->getString(); + else + pTextList[i] = OUString(); + break; + } + case DataPointCustomLabelFieldType_CELLREF: + { + // TODO: for now doesn't show placeholder + pTextList[i] = OUString(); + break; + } + case DataPointCustomLabelFieldType_TEXT: + { + pTextList[i] = aCustomLabels[i]->getString(); + break; + } + case DataPointCustomLabelFieldType_NEWLINE: + { + pTextList[i] = "\n"; + break; + } + default: + break; + } + aCustomLabels[i]->setString( aTextList[i] ); + } + } + else + { + auto pTextList = aTextList.getArray(); + if( pLabel->ShowCategoryName ) + { + pTextList[0] = getCategoryName( nPointIndex ); + } + + if( pLabel->ShowSeriesName ) + { + OUString aRole; + if ( m_xChartTypeModel ) + aRole = m_xChartTypeModel->getRoleOfSequenceForSeriesLabel(); + const rtl::Reference< DataSeries >& xSeries( rDataSeries.getModel() ); + pTextList[1] = DataSeriesHelper::getDataSeriesLabel( xSeries, aRole ); + } + + if( pLabel->ShowNumber ) + { + pTextList[2] = getLabelTextForValue(rDataSeries, nPointIndex, fValue, false); + } + + if( pLabel->ShowNumberInPercent ) + { + if(fSumValue==0.0) + fSumValue=1.0; + fValue /= fSumValue; + if( fValue < 0 ) + fValue*=-1.0; + + pTextList[3] = getLabelTextForValue(rDataSeries, nPointIndex, fValue, true); + } + } + + for( auto const & line : std::as_const(aTextList) ) + { + if( !line.isEmpty() ) + { + ++nLineCountForSymbolsize; + } + } + + //prepare properties for multipropertyset-interface of shape + tNameSequence* pPropNames; + tAnySequence* pPropValues; + if( !rDataSeries.getTextLabelMultiPropertyLists( nPointIndex, pPropNames, pPropValues ) ) + return xTextShape; + + // set text alignment for the text shape to be created. + LabelPositionHelper::changeTextAdjustment( *pPropValues, *pPropNames, eAlignment ); + + // check if data series entry percent value and absolute value have to + // be appended to the text label, and what should be the separator + // character (comma, space, new line). In case it is a new line we get + // a multi-line label. + bool bMultiLineLabel = ( aSeparator == "\n" ); + + if( bUseCustomLabel ) + { + Sequence< uno::Reference< XFormattedString > > aFormattedLabels( + comphelper::containerToSequence<uno::Reference<XFormattedString>>(aCustomLabels)); + + // create text shape + xTextShape = ShapeFactory:: + createText( xTarget_, aFormattedLabels, *pPropNames, *pPropValues, + ShapeFactory::makeTransformation( aScreenPosition2D ) ); + } + else + { + // join text list elements + OUStringBuffer aText; + for( sal_uInt32 nN = 0; nN < nTextListLength; ++nN) + { + if( !aTextList[nN].isEmpty() ) + { + if( !aText.isEmpty() ) + { + aText.append(aSeparator); + } + aText.append( aTextList[nN] ); + } + } + + //create text shape + xTextShape = ShapeFactory:: + createText( xTarget_, aText.makeStringAndClear(), *pPropNames, *pPropValues, + ShapeFactory::makeTransformation( aScreenPosition2D ) ); + } + + if( !xTextShape.is() ) + return xTextShape; + + // we need to use a default value for the maximum width property ? + if( nTextWidth == 0 && bTextWrap ) + { + sal_Int32 nMinSize = + (m_aPageReferenceSize.Height < m_aPageReferenceSize.Width) + ? m_aPageReferenceSize.Height + : m_aPageReferenceSize.Width; + nTextWidth = nMinSize / 3; + } + + // in case text must be wrapped set the maximum width property + // for the text shape + if( nTextWidth != 0 && bTextWrap ) + { + // compute the height of a line of text + if( !bMultiLineLabel || nLineCountForSymbolsize <= 0 ) + { + nLineCountForSymbolsize = 1; + } + awt::Size aTextSize = xTextShape->getSize(); + sal_Int32 aTextLineHeight = aTextSize.Height / nLineCountForSymbolsize; + + // set maximum text width + uno::Any aTextMaximumFrameWidth( nTextWidth ); + xTextShape->SvxShape::setPropertyValue( "TextMaximumFrameWidth", aTextMaximumFrameWidth ); + + // compute the total lines of text + aTextSize = xTextShape->getSize(); + nLineCountForSymbolsize = aTextSize.Height / aTextLineHeight; + } + + // in case text is rotated, the transformation property of the text + // shape is modified. + if( fRotationDegrees != 0.0 ) + { + const double fDegreesPi( -basegfx::deg2rad(fRotationDegrees) ); + xTextShape->SvxShape::setPropertyValue( "Transformation", ShapeFactory::makeTransformation( aScreenPosition2D, fDegreesPi ) ); + LabelPositionHelper::correctPositionForRotation( xTextShape, eAlignment, fRotationDegrees, true /*bRotateAroundCenter*/ ); + } + + awt::Point aTextShapePos(xTextShape->getPosition()); + if( m_bPieLabelsAllowToMove && rDataSeries.isLabelCustomPos(nPointIndex) ) + { + awt::Point aRelPos = rDataSeries.getLabelPosition(aTextShapePos, nPointIndex); + if( aRelPos.X != -1 ) + { + xTextShape->setPosition(aRelPos); + if( !m_xChartTypeModel->getChartType().equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_PIE) && + rDataSeries.getPropertiesOfSeries()->getPropertyValue( "ShowCustomLeaderLines" ).get<sal_Bool>()) + { + sal_Int32 nX1 = rScreenPosition2D.X; + sal_Int32 nY1 = rScreenPosition2D.Y; + sal_Int32 nX2 = nX1; + sal_Int32 nY2 = nY1; + ::basegfx::B2IRectangle aRect(BaseGFXHelper::makeRectangle(aRelPos, xTextShape->getSize())); + if (nX1 < aRelPos.X) + nX2 = aRelPos.X; + 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); + double fPageDiagonaleLength + = std::hypot(m_aPageReferenceSize.Width, m_aPageReferenceSize.Height); + if ((aLength.getLength() / fPageDiagonaleLength) >= 0.01) + { + drawing::PointSequenceSequence aPoints{ { {nX1, nY1}, {nX2, nY2} } }; + + VLineProperties aVLineProperties; + ShapeFactory::createLine2D(xTarget, aPoints, &aVLineProperties); + } + } + } + } + + // in case legend symbol has to be displayed, text shape position is + // slightly changed. + const awt::Point aUnrotatedTextPos(xTextShape->getPosition()); + if( xSymbol.is() ) + { + const awt::Point aOldTextPos( xTextShape->getPosition() ); + awt::Point aNewTextPos( aOldTextPos ); + + awt::Point aSymbolPosition( aUnrotatedTextPos ); + awt::Size aSymbolSize( xSymbol->getSize() ); + awt::Size aTextSize = xTextShape->getSize(); + + sal_Int32 nXDiff = aSymbolSize.Width + static_cast< sal_Int32 >( std::max( 100.0, fViewFontSize * 0.22 ) );//minimum 1mm + if( !bMultiLineLabel || nLineCountForSymbolsize <= 0 ) + nLineCountForSymbolsize = 1; + aSymbolPosition.Y += ((aTextSize.Height/nLineCountForSymbolsize)/4); + + if(eAlignment==LABEL_ALIGN_LEFT + || eAlignment==LABEL_ALIGN_LEFT_TOP + || eAlignment==LABEL_ALIGN_LEFT_BOTTOM) + { + aSymbolPosition.X -= nXDiff; + } + else if(eAlignment==LABEL_ALIGN_RIGHT + || eAlignment==LABEL_ALIGN_RIGHT_TOP + || eAlignment==LABEL_ALIGN_RIGHT_BOTTOM ) + { + aNewTextPos.X += nXDiff; + } + else if(eAlignment==LABEL_ALIGN_TOP + || eAlignment==LABEL_ALIGN_BOTTOM + || eAlignment==LABEL_ALIGN_CENTER ) + { + aSymbolPosition.X -= nXDiff/2; + aNewTextPos.X += nXDiff/2; + } + + xSymbol->setPosition( aSymbolPosition ); + xTextShape->setPosition( aNewTextPos ); + } + } + catch( const uno::Exception& ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + return xTextShape; +} + +namespace +{ +double lcl_getErrorBarLogicLength( + const uno::Sequence< double > & rData, + const uno::Reference< beans::XPropertySet >& xProp, + sal_Int32 nErrorBarStyle, + sal_Int32 nIndex, + bool bPositive, + bool bYError ) +{ + double fResult = std::numeric_limits<double>::quiet_NaN(); + try + { + switch( nErrorBarStyle ) + { + case css::chart::ErrorBarStyle::NONE: + break; + case css::chart::ErrorBarStyle::VARIANCE: + fResult = StatisticsHelper::getVariance( rData ); + break; + case css::chart::ErrorBarStyle::STANDARD_DEVIATION: + fResult = StatisticsHelper::getStandardDeviation( rData ); + break; + case css::chart::ErrorBarStyle::RELATIVE: + { + double fPercent = 0; + if( xProp->getPropertyValue( bPositive + ? OUString("PositiveError") + : OUString("NegativeError") ) >>= fPercent ) + { + if( nIndex >=0 && nIndex < rData.getLength() && + ! std::isnan( rData[nIndex] ) && + ! std::isnan( fPercent )) + { + fResult = rData[nIndex] * fPercent / 100.0; + } + } + } + break; + case css::chart::ErrorBarStyle::ABSOLUTE: + xProp->getPropertyValue( bPositive + ? OUString("PositiveError") + : OUString("NegativeError") ) >>= fResult; + break; + case css::chart::ErrorBarStyle::ERROR_MARGIN: + { + // todo: check if this is really what's called error-margin + double fPercent = 0; + if( xProp->getPropertyValue( bPositive + ? OUString("PositiveError") + : OUString("NegativeError") ) >>= fPercent ) + { + double fMaxValue = -std::numeric_limits<double>::infinity(); + for(double d : rData) + { + if(fMaxValue < d) + fMaxValue = d; + } + if( std::isfinite( fMaxValue ) && + std::isfinite( fPercent )) + { + fResult = fMaxValue * fPercent / 100.0; + } + } + } + break; + case css::chart::ErrorBarStyle::STANDARD_ERROR: + fResult = StatisticsHelper::getStandardError( rData ); + break; + case css::chart::ErrorBarStyle::FROM_DATA: + { + uno::Reference< chart2::data::XDataSource > xErrorBarData( xProp, uno::UNO_QUERY ); + if( xErrorBarData.is()) + fResult = StatisticsHelper::getErrorFromDataSource( + xErrorBarData, nIndex, bPositive, bYError); + } + break; + } + } + catch( const uno::Exception & ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + + return fResult; +} + +void lcl_AddErrorBottomLine( const drawing::Position3D& rPosition, ::basegfx::B2DVector aMainDirection + , std::vector<std::vector<css::drawing::Position3D>>& rPoly, sal_Int32 nSequenceIndex ) +{ + double fFixedWidth = 200.0; + + aMainDirection.normalize(); + ::basegfx::B2DVector aOrthoDirection(-aMainDirection.getY(),aMainDirection.getX()); + aOrthoDirection.normalize(); + + ::basegfx::B2DVector aAnchor( rPosition.PositionX, rPosition.PositionY ); + ::basegfx::B2DVector aStart = aAnchor + aOrthoDirection*fFixedWidth/2.0; + ::basegfx::B2DVector aEnd = aAnchor - aOrthoDirection*fFixedWidth/2.0; + + AddPointToPoly( rPoly, drawing::Position3D( aStart.getX(), aStart.getY(), rPosition.PositionZ), nSequenceIndex ); + AddPointToPoly( rPoly, drawing::Position3D( aEnd.getX(), aEnd.getY(), rPosition.PositionZ), nSequenceIndex ); +} + +::basegfx::B2DVector lcl_getErrorBarMainDirection( + const drawing::Position3D& rStart + , const drawing::Position3D& rBottomEnd + , PlottingPositionHelper const * pPosHelper + , const drawing::Position3D& rUnscaledLogicPosition + , bool bYError ) +{ + ::basegfx::B2DVector aMainDirection( rStart.PositionX - rBottomEnd.PositionX + , rStart.PositionY - rBottomEnd.PositionY ); + if( !aMainDirection.getLength() ) + { + //get logic clip values: + double MinX = pPosHelper->getLogicMinX(); + double MinY = pPosHelper->getLogicMinY(); + double MaxX = pPosHelper->getLogicMaxX(); + double MaxY = pPosHelper->getLogicMaxY(); + double fZ = pPosHelper->getLogicMinZ(); + + if( bYError ) + { + //main direction has constant x value + MinX = rUnscaledLogicPosition.PositionX; + MaxX = rUnscaledLogicPosition.PositionX; + } + else + { + //main direction has constant y value + MinY = rUnscaledLogicPosition.PositionY; + MaxY = rUnscaledLogicPosition.PositionY; + } + + drawing::Position3D aStart = pPosHelper->transformLogicToScene( MinX, MinY, fZ, false ); + drawing::Position3D aEnd = pPosHelper->transformLogicToScene( MaxX, MaxY, fZ, false ); + + aMainDirection = ::basegfx::B2DVector( aStart.PositionX - aEnd.PositionX + , aStart.PositionY - aEnd.PositionY ); + } + if( !aMainDirection.getLength() ) + { + //@todo + } + return aMainDirection; +} + +drawing::Position3D lcl_transformMixedToScene( PlottingPositionHelper const * pPosHelper + , double fX /*scaled*/, double fY /*unscaled*/, double fZ /*unscaled*/ ) +{ + if(!pPosHelper) + return drawing::Position3D(0,0,0); + pPosHelper->doLogicScaling( nullptr,&fY,&fZ ); + pPosHelper->clipScaledLogicValues( &fX,&fY,&fZ ); + return pPosHelper->transformScaledLogicToScene( fX, fY, fZ, false ); +} + +} // anonymous namespace + +void VSeriesPlotter::createErrorBar( + const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , const drawing::Position3D& rUnscaledLogicPosition + , const uno::Reference< beans::XPropertySet > & xErrorBarProperties + , const VDataSeries& rVDataSeries + , sal_Int32 nIndex + , bool bYError /* = true */ + , const double* pfScaledLogicX + ) +{ + if( !ChartTypeHelper::isSupportingStatisticProperties( m_xChartTypeModel, m_nDimension ) ) + return; + + if( ! xErrorBarProperties.is()) + return; + + try + { + bool bShowPositive = false; + bool bShowNegative = false; + sal_Int32 nErrorBarStyle = css::chart::ErrorBarStyle::VARIANCE; + + xErrorBarProperties->getPropertyValue( "ShowPositiveError") >>= bShowPositive; + xErrorBarProperties->getPropertyValue( "ShowNegativeError") >>= bShowNegative; + xErrorBarProperties->getPropertyValue( "ErrorBarStyle") >>= nErrorBarStyle; + + if(!bShowPositive && !bShowNegative) + return; + + if(nErrorBarStyle==css::chart::ErrorBarStyle::NONE) + return; + + if (!m_pPosHelper) + return; + + drawing::Position3D aUnscaledLogicPosition(rUnscaledLogicPosition); + if(nErrorBarStyle==css::chart::ErrorBarStyle::STANDARD_DEVIATION) + { + if (bYError) + aUnscaledLogicPosition.PositionY = rVDataSeries.getYMeanValue(); + else + aUnscaledLogicPosition.PositionX = rVDataSeries.getXMeanValue(); + } + + bool bCreateNegativeBorder = false;//make a vertical line at the negative end of the error bar + bool bCreatePositiveBorder = false;//make a vertical line at the positive end of the error bar + drawing::Position3D aMiddle(aUnscaledLogicPosition); + const double fX = aUnscaledLogicPosition.PositionX; + const double fY = aUnscaledLogicPosition.PositionY; + const double fZ = aUnscaledLogicPosition.PositionZ; + double fScaledX = fX; + if( pfScaledLogicX ) + fScaledX = *pfScaledLogicX; + else + m_pPosHelper->doLogicScaling( &fScaledX, nullptr, nullptr ); + + aMiddle = lcl_transformMixedToScene( m_pPosHelper, fScaledX, fY, fZ ); + + drawing::Position3D aNegative(aMiddle); + drawing::Position3D aPositive(aMiddle); + + uno::Sequence< double > aData( bYError ? rVDataSeries.getAllY() : rVDataSeries.getAllX() ); + + if( bShowPositive ) + { + double fLength = lcl_getErrorBarLogicLength( aData, xErrorBarProperties, nErrorBarStyle, nIndex, true, bYError ); + if( std::isfinite( fLength ) ) + { + double fLocalX = fX; + double fLocalY = fY; + if( bYError ) + { + fLocalY+=fLength; + aPositive = lcl_transformMixedToScene( m_pPosHelper, fScaledX, fLocalY, fZ ); + } + else + { + fLocalX+=fLength; + aPositive = m_pPosHelper->transformLogicToScene( fLocalX, fLocalY, fZ, true ); + } + bCreatePositiveBorder = m_pPosHelper->isLogicVisible(fLocalX, fLocalY, fZ); + } + else + bShowPositive = false; + } + + if( bShowNegative ) + { + double fLength = lcl_getErrorBarLogicLength( aData, xErrorBarProperties, nErrorBarStyle, nIndex, false, bYError ); + if( std::isfinite( fLength ) ) + { + double fLocalX = fX; + double fLocalY = fY; + if( bYError ) + { + fLocalY-=fLength; + aNegative = lcl_transformMixedToScene( m_pPosHelper, fScaledX, fLocalY, fZ ); + } + else + { + fLocalX-=fLength; + aNegative = m_pPosHelper->transformLogicToScene( fLocalX, fLocalY, fZ, true ); + } + if (std::isfinite(aNegative.PositionX) && + std::isfinite(aNegative.PositionY) && + std::isfinite(aNegative.PositionZ)) { + bCreateNegativeBorder = m_pPosHelper->isLogicVisible( fLocalX, fLocalY, fZ); + } else { + // If error bars result in a numerical problem (e.g., an + // error bar on a logarithmic chart that results in a point + // <= 0) then just turn off the error bar. + // + // TODO: This perhaps should display a warning, so the user + // knows why a bar is not appearing. + // TODO: This test could also be added to the positive case, + // though a numerical overflow there is less likely. + bShowNegative = false; + } + } + else + bShowNegative = false; + } + + if(!bShowPositive && !bShowNegative) + return; + + std::vector<std::vector<css::drawing::Position3D>> aPoly; + + sal_Int32 nSequenceIndex=0; + if( bShowNegative ) + AddPointToPoly( aPoly, aNegative, nSequenceIndex ); + AddPointToPoly( aPoly, aMiddle, nSequenceIndex ); + if( bShowPositive ) + AddPointToPoly( aPoly, aPositive, nSequenceIndex ); + + if( bShowNegative && bCreateNegativeBorder ) + { + ::basegfx::B2DVector aMainDirection = lcl_getErrorBarMainDirection( aMiddle, aNegative, m_pPosHelper, aUnscaledLogicPosition, bYError ); + nSequenceIndex++; + lcl_AddErrorBottomLine( aNegative, aMainDirection, aPoly, nSequenceIndex ); + } + if( bShowPositive && bCreatePositiveBorder ) + { + ::basegfx::B2DVector aMainDirection = lcl_getErrorBarMainDirection( aMiddle, aPositive, m_pPosHelper, aUnscaledLogicPosition, bYError ); + nSequenceIndex++; + lcl_AddErrorBottomLine( aPositive, aMainDirection, aPoly, nSequenceIndex ); + } + + rtl::Reference<SvxShapePolyPolygon> xShape = ShapeFactory::createLine2D( xTarget, aPoly ); + PropertyMapper::setMappedProperties( *xShape, xErrorBarProperties, PropertyMapper::getPropertyNameMapForLineProperties() ); + } + catch( const uno::Exception & ) + { + TOOLS_WARN_EXCEPTION("chart2", "" ); + } + +} + +void VSeriesPlotter::addErrorBorder( + const drawing::Position3D& rPos0 + ,const drawing::Position3D& rPos1 + ,const rtl::Reference<SvxShapeGroupAnyD>& rTarget + ,const uno::Reference< beans::XPropertySet >& rErrorBorderProp ) +{ + std::vector<std::vector<css::drawing::Position3D>> aPoly { { rPos0, rPos1} }; + rtl::Reference<SvxShapePolyPolygon> xShape = ShapeFactory::createLine2D( + rTarget, aPoly ); + PropertyMapper::setMappedProperties( *xShape, rErrorBorderProp, + PropertyMapper::getPropertyNameMapForLineProperties() ); +} + +void VSeriesPlotter::createErrorRectangle( + const drawing::Position3D& rUnscaledLogicPosition + ,VDataSeries& rVDataSeries + ,sal_Int32 nIndex + ,const rtl::Reference<SvxShapeGroupAnyD>& rTarget + ,bool bUseXErrorData + ,bool bUseYErrorData ) +{ + if ( m_nDimension != 2 ) + return; + + // error border properties + Reference< beans::XPropertySet > xErrorBorderPropX, xErrorBorderPropY; + if ( bUseXErrorData ) + { + xErrorBorderPropX = rVDataSeries.getXErrorBarProperties( nIndex ); + if ( !xErrorBorderPropX.is() ) + return; + } + rtl::Reference<SvxShapeGroupAnyD> xErrorBorder_ShapesX = + getErrorBarsGroupShape( rVDataSeries, rTarget, false ); + + if ( bUseYErrorData ) + { + xErrorBorderPropY = rVDataSeries.getYErrorBarProperties( nIndex ); + if ( !xErrorBorderPropY.is() ) + return; + } + rtl::Reference<SvxShapeGroupAnyD> xErrorBorder_ShapesY = + getErrorBarsGroupShape( rVDataSeries, rTarget, true ); + + if( !ChartTypeHelper::isSupportingStatisticProperties( m_xChartTypeModel, m_nDimension ) ) + return; + + try + { + bool bShowXPositive = false; + bool bShowXNegative = false; + bool bShowYPositive = false; + bool bShowYNegative = false; + + sal_Int32 nErrorBorderStyleX = css::chart::ErrorBarStyle::VARIANCE; + sal_Int32 nErrorBorderStyleY = css::chart::ErrorBarStyle::VARIANCE; + + if ( bUseXErrorData ) + { + xErrorBorderPropX->getPropertyValue( "ErrorBarStyle" ) >>= nErrorBorderStyleX; + xErrorBorderPropX->getPropertyValue( "ShowPositiveError") >>= bShowXPositive; + xErrorBorderPropX->getPropertyValue( "ShowNegativeError") >>= bShowXNegative; + } + if ( bUseYErrorData ) + { + xErrorBorderPropY->getPropertyValue( "ErrorBarStyle" ) >>= nErrorBorderStyleY; + xErrorBorderPropY->getPropertyValue( "ShowPositiveError") >>= bShowYPositive; + xErrorBorderPropY->getPropertyValue( "ShowNegativeError") >>= bShowYNegative; + } + + if ( bUseXErrorData && nErrorBorderStyleX == css::chart::ErrorBarStyle::NONE ) + bUseXErrorData = false; + if ( bUseYErrorData && nErrorBorderStyleY == css::chart::ErrorBarStyle::NONE ) + bUseYErrorData = false; + + if ( !bShowXPositive && !bShowXNegative && !bShowYPositive && !bShowYNegative ) + return; + + if ( !m_pPosHelper ) + return; + + drawing::Position3D aUnscaledLogicPosition( rUnscaledLogicPosition ); + if ( bUseXErrorData && nErrorBorderStyleX == css::chart::ErrorBarStyle::STANDARD_DEVIATION ) + aUnscaledLogicPosition.PositionX = rVDataSeries.getXMeanValue(); + if ( bUseYErrorData && nErrorBorderStyleY == css::chart::ErrorBarStyle::STANDARD_DEVIATION ) + aUnscaledLogicPosition.PositionY = rVDataSeries.getYMeanValue(); + + const double fX = aUnscaledLogicPosition.PositionX; + const double fY = aUnscaledLogicPosition.PositionY; + const double fZ = aUnscaledLogicPosition.PositionZ; + double fScaledX = fX; + m_pPosHelper->doLogicScaling( &fScaledX, nullptr, nullptr ); + + uno::Sequence< double > aDataX( rVDataSeries.getAllX() ); + uno::Sequence< double > aDataY( rVDataSeries.getAllY() ); + + double fPosX = 0.0; + double fPosY = 0.0; + double fNegX = 0.0; + double fNegY = 0.0; + if ( bUseXErrorData ) + { + if ( bShowXPositive ) + fPosX = lcl_getErrorBarLogicLength( aDataX, xErrorBorderPropX, + nErrorBorderStyleX, nIndex, true, false ); + if ( bShowXNegative ) + fNegX = lcl_getErrorBarLogicLength( aDataX, xErrorBorderPropX, + nErrorBorderStyleX, nIndex, false, false ); + } + + if ( bUseYErrorData ) + { + if ( bShowYPositive ) + fPosY = lcl_getErrorBarLogicLength( aDataY, xErrorBorderPropY, + nErrorBorderStyleY, nIndex, true, true ); + if ( bShowYNegative ) + fNegY = lcl_getErrorBarLogicLength( aDataY, xErrorBorderPropY, + nErrorBorderStyleY, nIndex, false, true ); + } + + if ( !( std::isfinite( fPosX ) && + std::isfinite( fPosY ) && + std::isfinite( fNegX ) && + std::isfinite( fNegY ) ) ) + return; + + drawing::Position3D aBottomLeft( lcl_transformMixedToScene( m_pPosHelper, + fX - fNegX, fY - fNegY, fZ ) ); + drawing::Position3D aTopLeft( lcl_transformMixedToScene( m_pPosHelper, + fX - fNegX, fY + fPosY, fZ ) ); + drawing::Position3D aTopRight( lcl_transformMixedToScene( m_pPosHelper, + fX + fPosX, fY + fPosY, fZ ) ); + drawing::Position3D aBottomRight( lcl_transformMixedToScene( m_pPosHelper, + fX + fPosX, fY - fNegY, fZ ) ); + if ( bUseXErrorData ) + { + // top border + addErrorBorder( aTopLeft, aTopRight, xErrorBorder_ShapesX, xErrorBorderPropX ); + + // bottom border + addErrorBorder( aBottomRight, aBottomLeft, xErrorBorder_ShapesX, xErrorBorderPropX ); + } + + if ( bUseYErrorData ) + { + // left border + addErrorBorder( aBottomLeft, aTopLeft, xErrorBorder_ShapesY, xErrorBorderPropY ); + + // right border + addErrorBorder( aTopRight, aBottomRight, xErrorBorder_ShapesY, xErrorBorderPropY ); + } + } + catch( const uno::Exception & ) + { + DBG_UNHANDLED_EXCEPTION("chart2", "Exception in createErrorRectangle(). "); + } +} + +void VSeriesPlotter::createErrorBar_X( const drawing::Position3D& rUnscaledLogicPosition + , VDataSeries& rVDataSeries, sal_Int32 nPointIndex + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + if(m_nDimension!=2) + return; + // error bars + uno::Reference< beans::XPropertySet > xErrorBarProp(rVDataSeries.getXErrorBarProperties(nPointIndex)); + if( xErrorBarProp.is()) + { + rtl::Reference<SvxShapeGroupAnyD> xErrorBarsGroup_Shapes = + getErrorBarsGroupShape(rVDataSeries, xTarget, false); + + createErrorBar( xErrorBarsGroup_Shapes + , rUnscaledLogicPosition, xErrorBarProp + , rVDataSeries, nPointIndex + , false /* bYError */ + , nullptr ); + } +} + +void VSeriesPlotter::createErrorBar_Y( const drawing::Position3D& rUnscaledLogicPosition + , VDataSeries& rVDataSeries, sal_Int32 nPointIndex + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , double const * pfScaledLogicX ) +{ + if(m_nDimension!=2) + return; + // error bars + uno::Reference< beans::XPropertySet > xErrorBarProp(rVDataSeries.getYErrorBarProperties(nPointIndex)); + if( xErrorBarProp.is()) + { + rtl::Reference<SvxShapeGroupAnyD> xErrorBarsGroup_Shapes = + getErrorBarsGroupShape(rVDataSeries, xTarget, true); + + createErrorBar( xErrorBarsGroup_Shapes + , rUnscaledLogicPosition, xErrorBarProp + , rVDataSeries, nPointIndex + , true /* bYError */ + , pfScaledLogicX ); + } +} + +void VSeriesPlotter::createRegressionCurvesShapes( VDataSeries const & rVDataSeries, + const rtl::Reference<SvxShapeGroupAnyD>& xTarget, + const rtl::Reference<SvxShapeGroupAnyD>& xEquationTarget, + bool bMaySkipPoints ) +{ + if(m_nDimension!=2) + return; + rtl::Reference< DataSeries > xContainer( rVDataSeries.getModel() ); + if(!xContainer.is()) + return; + + if (!m_pPosHelper) + return; + + const std::vector< rtl::Reference< ::chart::RegressionCurveModel > > & aCurveList = xContainer->getRegressionCurves2(); + + for(std::size_t nN=0; nN<aCurveList.size(); nN++) + { + const auto & rCurve = aCurveList[nN]; + uno::Reference< XRegressionCurveCalculator > xCalculator( rCurve->getCalculator() ); + if( !xCalculator.is()) + continue; + + bool bAverageLine = RegressionCurveHelper::isMeanValueLine( rCurve ); + + sal_Int32 aDegree = 2; + sal_Int32 aPeriod = 2; + sal_Int32 aMovingAverageType = css::chart2::MovingAverageType::Prior; + double aExtrapolateForward = 0.0; + double aExtrapolateBackward = 0.0; + bool bForceIntercept = false; + double aInterceptValue = 0.0; + + if ( !bAverageLine ) + { + rCurve->getPropertyValue( "PolynomialDegree") >>= aDegree; + rCurve->getPropertyValue( "MovingAveragePeriod") >>= aPeriod; + rCurve->getPropertyValue( "MovingAverageType") >>= aMovingAverageType; + rCurve->getPropertyValue( "ExtrapolateForward") >>= aExtrapolateForward; + rCurve->getPropertyValue( "ExtrapolateBackward") >>= aExtrapolateBackward; + rCurve->getPropertyValue( "ForceIntercept") >>= bForceIntercept; + if (bForceIntercept) + rCurve->getPropertyValue( "InterceptValue") >>= aInterceptValue; + } + + double fChartMinX = m_pPosHelper->getLogicMinX(); + double fChartMaxX = m_pPosHelper->getLogicMaxX(); + + double fMinX = fChartMinX; + double fMaxX = fChartMaxX; + + double fPointScale = 1.0; + + if( !bAverageLine ) + { + rVDataSeries.getMinMaxXValue(fMinX, fMaxX); + fMaxX += aExtrapolateForward; + fMinX -= aExtrapolateBackward; + + fPointScale = (fMaxX - fMinX) / (fChartMaxX - fChartMinX); + // sanitize the value, tdf#119922 + fPointScale = std::min(fPointScale, 1000.0); + } + + xCalculator->setRegressionProperties(aDegree, bForceIntercept, aInterceptValue, aPeriod, + aMovingAverageType); + xCalculator->recalculateRegression(rVDataSeries.getAllX(), rVDataSeries.getAllY()); + sal_Int32 nPointCount = 100 * fPointScale; + + if ( nPointCount < 2 ) + nPointCount = 2; + + std::vector< ExplicitScaleData > aScales( m_pPosHelper->getScales()); + uno::Reference< chart2::XScaling > xScalingX; + uno::Reference< chart2::XScaling > xScalingY; + if( aScales.size() >= 2 ) + { + xScalingX.set( aScales[0].Scaling ); + xScalingY.set( aScales[1].Scaling ); + } + + const uno::Sequence< geometry::RealPoint2D > aCalculatedPoints( + xCalculator->getCurveValues( + fMinX, fMaxX, nPointCount, + xScalingX, xScalingY, bMaySkipPoints )); + + nPointCount = aCalculatedPoints.getLength(); + + drawing::PolyPolygonShape3D aRegressionPoly; + aRegressionPoly.SequenceX.realloc(1); + aRegressionPoly.SequenceY.realloc(1); + aRegressionPoly.SequenceZ.realloc(1); + auto pSequenceX = aRegressionPoly.SequenceX.getArray(); + auto pSequenceY = aRegressionPoly.SequenceY.getArray(); + auto pSequenceZ = aRegressionPoly.SequenceZ.getArray(); + pSequenceX[0].realloc(nPointCount); + pSequenceY[0].realloc(nPointCount); + auto pSequenceX0 = pSequenceX[0].getArray(); + auto pSequenceY0 = pSequenceY[0].getArray(); + + sal_Int32 nRealPointCount = 0; + + for(geometry::RealPoint2D const & p : aCalculatedPoints) + { + double fLogicX = p.X; + double fLogicY = p.Y; + double fLogicZ = 0.0; //dummy + + // fdo#51656: don't scale mean value lines + if(!bAverageLine) + m_pPosHelper->doLogicScaling( &fLogicX, &fLogicY, &fLogicZ ); + + if(!std::isnan(fLogicX) && !std::isinf(fLogicX) && + !std::isnan(fLogicY) && !std::isinf(fLogicY) && + !std::isnan(fLogicZ) && !std::isinf(fLogicZ) ) + { + pSequenceX0[nRealPointCount] = fLogicX; + pSequenceY0[nRealPointCount] = fLogicY; + nRealPointCount++; + } + } + pSequenceX[0].realloc(nRealPointCount); + pSequenceY[0].realloc(nRealPointCount); + pSequenceZ[0].realloc(nRealPointCount); + + drawing::PolyPolygonShape3D aClippedPoly; + Clipping::clipPolygonAtRectangle( aRegressionPoly, m_pPosHelper->getScaledLogicClipDoubleRect(), aClippedPoly ); + aRegressionPoly = aClippedPoly; + m_pPosHelper->transformScaledLogicToScene( aRegressionPoly ); + + awt::Point aDefaultPos; + if( aRegressionPoly.SequenceX.hasElements() && aRegressionPoly.SequenceX[0].hasElements() ) + { + VLineProperties aVLineProperties; + aVLineProperties.initFromPropertySet( rCurve ); + + //create an extra group shape for each curve for selection handling + rtl::Reference<SvxShapeGroupAnyD> xRegressionGroupShapes = + createGroupShape( xTarget, rVDataSeries.getDataCurveCID( nN, bAverageLine ) ); + rtl::Reference<SvxShapePolyPolygon> xShape = ShapeFactory::createLine2D( + xRegressionGroupShapes, PolyToPointSequence( aRegressionPoly ), &aVLineProperties ); + ShapeFactory::setShapeName( xShape, "MarkHandles" ); + aDefaultPos = xShape->getPosition(); + } + + // curve equation and correlation coefficient + uno::Reference< beans::XPropertySet > xEquationProperties( rCurve->getEquationProperties()); + if( xEquationProperties.is()) + { + createRegressionCurveEquationShapes( + rVDataSeries.getDataCurveEquationCID( nN ), + xEquationProperties, xEquationTarget, xCalculator, + aDefaultPos ); + } + } +} + +static sal_Int32 lcl_getOUStringMaxLineLength ( OUStringBuffer const & aString ) +{ + const sal_Int32 nStringLength = aString.getLength(); + sal_Int32 nMaxLineLength = 0; + + for ( sal_Int32 i=0; i<nStringLength; i++ ) + { + sal_Int32 indexSep = aString.indexOf( "\n", i ); + if ( indexSep < 0 ) + indexSep = nStringLength; + sal_Int32 nLineLength = indexSep - i; + if ( nLineLength > nMaxLineLength ) + nMaxLineLength = nLineLength; + i = indexSep; + } + + return nMaxLineLength; +} + +void VSeriesPlotter::createRegressionCurveEquationShapes( + const OUString & rEquationCID, + const uno::Reference< beans::XPropertySet > & xEquationProperties, + const rtl::Reference<SvxShapeGroupAnyD>& xEquationTarget, + const uno::Reference< chart2::XRegressionCurveCalculator > & xRegressionCurveCalculator, + awt::Point aDefaultPos ) +{ + OSL_ASSERT( xEquationProperties.is()); + if( !xEquationProperties.is()) + return; + + bool bShowEquation = false; + bool bShowCorrCoeff = false; + if(!(( xEquationProperties->getPropertyValue( "ShowEquation") >>= bShowEquation ) && + ( xEquationProperties->getPropertyValue( "ShowCorrelationCoefficient") >>= bShowCorrCoeff ))) + return; + + if( ! (bShowEquation || bShowCorrCoeff)) + return; + + OUStringBuffer aFormula; + sal_Int32 nNumberFormatKey = 0; + sal_Int32 nFormulaWidth = 0; + xEquationProperties->getPropertyValue(CHART_UNONAME_NUMFMT) >>= nNumberFormatKey; + bool bResizeEquation = true; + sal_Int32 nMaxIteration = 2; + if ( bShowEquation ) + { + OUString aXName, aYName; + if ( !(xEquationProperties->getPropertyValue( "XName" ) >>= aXName) ) + aXName = OUString( "x" ); + if ( !(xEquationProperties->getPropertyValue( "YName" ) >>= aYName) ) + aYName = OUString( "f(x)" ); + xRegressionCurveCalculator->setXYNames( aXName, aYName ); + } + + for ( sal_Int32 nCountIteration = 0; bResizeEquation && nCountIteration < nMaxIteration ; nCountIteration++ ) + { + bResizeEquation = false; + if( bShowEquation ) + { + if (m_apNumberFormatterWrapper) + { // iteration 0: default representation (no wrap) + // iteration 1: expected width (nFormulaWidth) is calculated + aFormula = xRegressionCurveCalculator->getFormattedRepresentation( + m_apNumberFormatterWrapper->getNumberFormatsSupplier(), + nNumberFormatKey, nFormulaWidth ); + nFormulaWidth = lcl_getOUStringMaxLineLength( aFormula ); + } + else + { + aFormula = xRegressionCurveCalculator->getRepresentation(); + } + + if( bShowCorrCoeff ) + { + aFormula.append( "\n" ); + } + } + if( bShowCorrCoeff ) + { + aFormula.append( "R" + OUStringChar( aSuperscriptFigures[2] ) + " = " ); + double fR( xRegressionCurveCalculator->getCorrelationCoefficient()); + if (m_apNumberFormatterWrapper) + { + Color nLabelCol; + bool bColChanged; + aFormula.append( + m_apNumberFormatterWrapper->getFormattedString( + nNumberFormatKey, fR*fR, nLabelCol, bColChanged )); + //@todo: change color of label if bColChanged is true + } + else + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + const OUString& aNumDecimalSep = rLocaleDataWrapper.getNumDecimalSep(); + sal_Unicode aDecimalSep = aNumDecimalSep[0]; + aFormula.append( ::rtl::math::doubleToUString( + fR*fR, rtl_math_StringFormat_G, 4, aDecimalSep, true )); + } + } + + awt::Point aScreenPosition2D; + chart2::RelativePosition aRelativePosition; + if( xEquationProperties->getPropertyValue( "RelativePosition") >>= aRelativePosition ) + { + //@todo decide whether x is primary or secondary + double fX = aRelativePosition.Primary*m_aPageReferenceSize.Width; + double fY = aRelativePosition.Secondary*m_aPageReferenceSize.Height; + aScreenPosition2D.X = static_cast< sal_Int32 >( ::rtl::math::round( fX )); + aScreenPosition2D.Y = static_cast< sal_Int32 >( ::rtl::math::round( fY )); + } + else + aScreenPosition2D = aDefaultPos; + + if( !aFormula.isEmpty()) + { + // set fill and line properties on creation + tNameSequence aNames; + tAnySequence aValues; + PropertyMapper::getPreparedTextShapePropertyLists( xEquationProperties, aNames, aValues ); + + rtl::Reference<SvxShapeText> xTextShape = ShapeFactory::createText( + xEquationTarget, aFormula.makeStringAndClear(), + aNames, aValues, ShapeFactory::makeTransformation( aScreenPosition2D )); + + ShapeFactory::setShapeName( xTextShape, rEquationCID ); + awt::Size aSize( xTextShape->getSize() ); + awt::Point aPos( RelativePositionHelper::getUpperLeftCornerOfAnchoredObject( + aScreenPosition2D, aSize, aRelativePosition.Anchor ) ); + //ensure that the equation is fully placed within the page (if possible) + if( (aPos.X + aSize.Width) > m_aPageReferenceSize.Width ) + aPos.X = m_aPageReferenceSize.Width - aSize.Width; + if( aPos.X < 0 ) + { + aPos.X = 0; + if ( nFormulaWidth > 0 ) + { + bResizeEquation = true; + if ( nCountIteration < nMaxIteration-1 ) + xEquationTarget->remove( xTextShape ); // remove equation + nFormulaWidth *= m_aPageReferenceSize.Width / static_cast< double >(aSize.Width); + nFormulaWidth -= nCountIteration; + if ( nFormulaWidth < 0 ) + nFormulaWidth = 0; + } + } + if( (aPos.Y + aSize.Height) > m_aPageReferenceSize.Height ) + aPos.Y = m_aPageReferenceSize.Height - aSize.Height; + if( aPos.Y < 0 ) + aPos.Y = 0; + if ( !bResizeEquation || nCountIteration == nMaxIteration-1 ) + xTextShape->setPosition(aPos); // if equation was not removed + } + } +} + +void VSeriesPlotter::setTimeResolutionOnXAxis( tools::Long TimeResolution, const Date& rNullDate ) +{ + m_nTimeResolution = TimeResolution; + m_aNullDate = rNullDate; +} + +// MinimumAndMaximumSupplier +tools::Long VSeriesPlotter::calculateTimeResolutionOnXAxis() +{ + tools::Long nRet = css::chart::TimeUnit::YEAR; + if (!m_pExplicitCategoriesProvider) + return nRet; + + const std::vector<double>& rDateCategories = m_pExplicitCategoriesProvider->getDateCategories(); + if (rDateCategories.empty()) + return nRet; + + std::vector<double>::const_iterator aIt = rDateCategories.begin(), aEnd = rDateCategories.end(); + + aIt = std::find_if(aIt, aEnd, [](const double& rDateCategory) { return !std::isnan(rDateCategory); }); + if (aIt == aEnd) + return nRet; + + Date aNullDate(30,12,1899); + if (m_apNumberFormatterWrapper) + aNullDate = m_apNumberFormatterWrapper->getNullDate(); + + Date aPrevious(aNullDate); aPrevious.AddDays(rtl::math::approxFloor(*aIt)); + ++aIt; + for(;aIt!=aEnd;++aIt) + { + if (std::isnan(*aIt)) + continue; + + Date aCurrent(aNullDate); aCurrent.AddDays(rtl::math::approxFloor(*aIt)); + if( nRet == css::chart::TimeUnit::YEAR ) + { + if( DateHelper::IsInSameYear( aPrevious, aCurrent ) ) + nRet = css::chart::TimeUnit::MONTH; + } + if( nRet == css::chart::TimeUnit::MONTH ) + { + if( DateHelper::IsInSameMonth( aPrevious, aCurrent ) ) + nRet = css::chart::TimeUnit::DAY; + } + if( nRet == css::chart::TimeUnit::DAY ) + break; + aPrevious=aCurrent; + } + + return nRet; +} +double VSeriesPlotter::getMinimumX() +{ + double fMinimum, fMaximum; + getMinimumAndMaximumX( fMinimum, fMaximum ); + return fMinimum; +} +double VSeriesPlotter::getMaximumX() +{ + double fMinimum, fMaximum; + getMinimumAndMaximumX( fMinimum, fMaximum ); + return fMaximum; +} + +double VSeriesPlotter::getMinimumYInRange( double fMinimumX, double fMaximumX, sal_Int32 nAxisIndex ) +{ + if( !m_bCategoryXAxis || ( m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis() ) ) + { + double fMinY, fMaxY; + getMinimumAndMaximumYInContinuousXRange( fMinY, fMaxY, fMinimumX, fMaximumX, nAxisIndex ); + return fMinY; + } + + double fMinimum = std::numeric_limits<double>::infinity(); + double fMaximum = -std::numeric_limits<double>::infinity(); + for(std::vector<VDataSeriesGroup> & rXSlots : m_aZSlots) + { + for(VDataSeriesGroup & rXSlot : rXSlots) + { + double fLocalMinimum, fLocalMaximum; + rXSlot.calculateYMinAndMaxForCategoryRange( + static_cast<sal_Int32>(fMinimumX-1.0) //first category (index 0) matches with real number 1.0 + , static_cast<sal_Int32>(fMaximumX-1.0) //first category (index 0) matches with real number 1.0 + , isSeparateStackingForDifferentSigns( 1 ) + , fLocalMinimum, fLocalMaximum, nAxisIndex ); + if(fMaximum<fLocalMaximum) + fMaximum=fLocalMaximum; + if(fMinimum>fLocalMinimum) + fMinimum=fLocalMinimum; + } + } + if(std::isinf(fMinimum)) + return std::numeric_limits<double>::quiet_NaN(); + return fMinimum; +} + +double VSeriesPlotter::getMaximumYInRange( double fMinimumX, double fMaximumX, sal_Int32 nAxisIndex ) +{ + if( !m_bCategoryXAxis || ( m_pExplicitCategoriesProvider && m_pExplicitCategoriesProvider->isDateAxis() ) ) + { + double fMinY, fMaxY; + getMinimumAndMaximumYInContinuousXRange( fMinY, fMaxY, fMinimumX, fMaximumX, nAxisIndex ); + return fMaxY; + } + + double fMinimum = std::numeric_limits<double>::infinity(); + double fMaximum = -std::numeric_limits<double>::infinity(); + for( std::vector< VDataSeriesGroup > & rXSlots : m_aZSlots) + { + for(VDataSeriesGroup & rXSlot : rXSlots) + { + double fLocalMinimum, fLocalMaximum; + rXSlot.calculateYMinAndMaxForCategoryRange( + static_cast<sal_Int32>(fMinimumX-1.0) //first category (index 0) matches with real number 1.0 + , static_cast<sal_Int32>(fMaximumX-1.0) //first category (index 0) matches with real number 1.0 + , isSeparateStackingForDifferentSigns( 1 ) + , fLocalMinimum, fLocalMaximum, nAxisIndex ); + if(fMaximum<fLocalMaximum) + fMaximum=fLocalMaximum; + if(fMinimum>fLocalMinimum) + fMinimum=fLocalMinimum; + } + } + if(std::isinf(fMaximum)) + return std::numeric_limits<double>::quiet_NaN(); + return fMaximum; +} + +double VSeriesPlotter::getMinimumZ() +{ + //this is the default for all charts without a meaningful z axis + return 1.0; +} +double VSeriesPlotter::getMaximumZ() +{ + if( m_nDimension!=3 || m_aZSlots.empty() ) + return getMinimumZ()+1; + return m_aZSlots.size(); +} + +namespace +{ + bool lcl_isValueAxis( sal_Int32 nDimensionIndex, bool bCategoryXAxis ) + { + // default implementation: true for Y axes, and for value X axis + if( nDimensionIndex == 0 ) + return !bCategoryXAxis; + return nDimensionIndex == 1; + } +} + +bool VSeriesPlotter::isExpandBorderToIncrementRhythm( sal_Int32 nDimensionIndex ) +{ + return lcl_isValueAxis( nDimensionIndex, m_bCategoryXAxis ); +} + +bool VSeriesPlotter::isExpandIfValuesCloseToBorder( sal_Int32 nDimensionIndex ) +{ + // do not expand axes in 3D charts + return (m_nDimension < 3) && lcl_isValueAxis( nDimensionIndex, m_bCategoryXAxis ); +} + +bool VSeriesPlotter::isExpandWideValuesToZero( sal_Int32 nDimensionIndex ) +{ + // default implementation: only for Y axis + return nDimensionIndex == 1; +} + +bool VSeriesPlotter::isExpandNarrowValuesTowardZero( sal_Int32 nDimensionIndex ) +{ + // default implementation: only for Y axis + return nDimensionIndex == 1; +} + +bool VSeriesPlotter::isSeparateStackingForDifferentSigns( sal_Int32 nDimensionIndex ) +{ + // default implementation: only for Y axis + return nDimensionIndex == 1; +} + +void VSeriesPlotter::getMinimumAndMaximumX( double& rfMinimum, double& rfMaximum ) const +{ + rfMinimum = std::numeric_limits<double>::infinity(); + rfMaximum = -std::numeric_limits<double>::infinity(); + + for (auto const& ZSlot : m_aZSlots) + { + for (auto const& XSlot : ZSlot) + { + double fLocalMinimum, fLocalMaximum; + XSlot.getMinimumAndMaximumX( fLocalMinimum, fLocalMaximum ); + if( !std::isnan(fLocalMinimum) && fLocalMinimum< rfMinimum ) + rfMinimum = fLocalMinimum; + if( !std::isnan(fLocalMaximum) && fLocalMaximum> rfMaximum ) + rfMaximum = fLocalMaximum; + } + } + if(std::isinf(rfMinimum)) + rfMinimum = std::numeric_limits<double>::quiet_NaN(); + if(std::isinf(rfMaximum)) + rfMaximum = std::numeric_limits<double>::quiet_NaN(); +} + +void VSeriesPlotter::getMinimumAndMaximumYInContinuousXRange( double& rfMinY, double& rfMaxY, double fMinX, double fMaxX, sal_Int32 nAxisIndex ) const +{ + rfMinY = std::numeric_limits<double>::infinity(); + rfMaxY = -std::numeric_limits<double>::infinity(); + + for (auto const& ZSlot : m_aZSlots) + { + for (auto const& XSlot : ZSlot) + { + double fLocalMinimum, fLocalMaximum; + XSlot.getMinimumAndMaximumYInContinuousXRange( fLocalMinimum, fLocalMaximum, fMinX, fMaxX, nAxisIndex ); + if( !std::isnan(fLocalMinimum) && fLocalMinimum< rfMinY ) + rfMinY = fLocalMinimum; + if( !std::isnan(fLocalMaximum) && fLocalMaximum> rfMaxY ) + rfMaxY = fLocalMaximum; + } + } + if(std::isinf(rfMinY)) + rfMinY = std::numeric_limits<double>::quiet_NaN(); + if(std::isinf(rfMaxY)) + rfMaxY = std::numeric_limits<double>::quiet_NaN(); +} + +sal_Int32 VSeriesPlotter::getPointCount() const +{ + sal_Int32 nRet = 0; + + for (auto const& ZSlot : m_aZSlots) + { + for (auto const& XSlot : ZSlot) + { + sal_Int32 nPointCount = XSlot.getPointCount(); + if( nPointCount>nRet ) + nRet = nPointCount; + } + } + return nRet; +} + +void VSeriesPlotter::setNumberFormatsSupplier( + const uno::Reference< util::XNumberFormatsSupplier > & xNumFmtSupplier ) +{ + m_apNumberFormatterWrapper.reset( new NumberFormatterWrapper( xNumFmtSupplier )); +} + +void VSeriesPlotter::setColorScheme( const uno::Reference< XColorScheme >& xColorScheme ) +{ + m_xColorScheme = xColorScheme; +} + +void VSeriesPlotter::setExplicitCategoriesProvider( ExplicitCategoriesProvider* pExplicitCategoriesProvider ) +{ + m_pExplicitCategoriesProvider = pExplicitCategoriesProvider; +} + +sal_Int32 VDataSeriesGroup::getPointCount() const +{ + if(!m_bMaxPointCountDirty) + return m_nMaxPointCount; + + sal_Int32 nRet = 0; + + for (std::unique_ptr<VDataSeries> const & pSeries : m_aSeriesVector) + { + sal_Int32 nPointCount = pSeries->getTotalPointCount(); + if( nPointCount>nRet ) + nRet = nPointCount; + } + m_nMaxPointCount=nRet; + m_aListOfCachedYValues.clear(); + m_aListOfCachedYValues.resize(m_nMaxPointCount); + m_bMaxPointCountDirty=false; + return nRet; +} + +sal_Int32 VDataSeriesGroup::getAttachedAxisIndexForFirstSeries() const +{ + sal_Int32 nRet = 0; + + if (!m_aSeriesVector.empty()) + nRet = m_aSeriesVector[0]->getAttachedAxisIndex(); + + return nRet; +} + +void VDataSeriesGroup::getMinimumAndMaximumX( double& rfMinimum, double& rfMaximum ) const +{ + + rfMinimum = std::numeric_limits<double>::infinity(); + rfMaximum = -std::numeric_limits<double>::infinity(); + + for (std::unique_ptr<VDataSeries> const & pSeries : m_aSeriesVector) + { + sal_Int32 nPointCount = pSeries->getTotalPointCount(); + for(sal_Int32 nN=0;nN<nPointCount;nN++) + { + double fX = pSeries->getXValue( nN ); + if( std::isnan(fX) ) + continue; + if(rfMaximum<fX) + rfMaximum=fX; + if(rfMinimum>fX) + rfMinimum=fX; + } + } + if(std::isinf(rfMinimum)) + rfMinimum = std::numeric_limits<double>::quiet_NaN(); + if(std::isinf(rfMaximum)) + rfMaximum = std::numeric_limits<double>::quiet_NaN(); +} + +namespace { + +/** + * Keep track of minimum and maximum Y values for one or more data series. + * When multiple data series exist, that indicates that the data series are + * stacked. + * + * <p>For each X value, we calculate separate Y value ranges for each data + * series in the first pass. In the second pass, we calculate the minimum Y + * value by taking the absolute minimum value of all data series, whereas + * the maximum Y value is the sum of all the series maximum Y values.</p> + * + * <p>Once that's done for all X values, the final min / max Y values get + * calculated by taking the absolute min / max Y values across all the X + * values.</p> + */ +class PerXMinMaxCalculator +{ + typedef std::pair<double, double> MinMaxType; + typedef std::map<size_t, MinMaxType> SeriesMinMaxType; + typedef std::map<double, SeriesMinMaxType> GroupMinMaxType; + typedef std::unordered_map<double, MinMaxType> TotalStoreType; + GroupMinMaxType m_SeriesGroup; + size_t mnCurSeries; + +public: + PerXMinMaxCalculator() : mnCurSeries(0) {} + + void nextSeries() { ++mnCurSeries; } + + void setValue(double fX, double fY) + { + SeriesMinMaxType* pStore = getByXValue(fX); // get storage for given X value. + if (!pStore) + // This shouldn't happen! + return; + + SeriesMinMaxType::iterator it = pStore->lower_bound(mnCurSeries); + if (it != pStore->end() && !pStore->key_comp()(mnCurSeries, it->first)) + { + MinMaxType& r = it->second; + // A min-max pair already exists for this series. Update it. + if (fY < r.first) + r.first = fY; + if (r.second < fY) + r.second = fY; + } + else + { + // No existing pair. Insert a new one. + pStore->insert( + it, SeriesMinMaxType::value_type( + mnCurSeries, MinMaxType(fY,fY))); + } + } + + void getTotalRange(double& rfMin, double& rfMax) const + { + TotalStoreType aStore; + getTotalStore(aStore); + + if (aStore.empty()) + { + rfMin = std::numeric_limits<double>::quiet_NaN(); + rfMax = std::numeric_limits<double>::quiet_NaN(); + return; + } + + TotalStoreType::const_iterator it = aStore.begin(), itEnd = aStore.end(); + rfMin = it->second.first; + rfMax = it->second.second; + for (++it; it != itEnd; ++it) + { + if (rfMin > it->second.first) + rfMin = it->second.first; + if (rfMax < it->second.second) + rfMax = it->second.second; + } + } + +private: + /** + * Parse all data and reduce them into a set of global Y value ranges per + * X value. + */ + void getTotalStore(TotalStoreType& rStore) const + { + TotalStoreType aStore; + for (auto const& it : m_SeriesGroup) + { + double fX = it.first; + + const SeriesMinMaxType& rSeries = it.second; + for (auto const& series : rSeries) + { + double fYMin = series.second.first, fYMax = series.second.second; + TotalStoreType::iterator itr = aStore.find(fX); + if (itr == aStore.end()) + // New min-max pair for give X value. + aStore.emplace(fX, std::pair<double,double>(fYMin,fYMax)); + else + { + MinMaxType& r = itr->second; + if (fYMin < r.first) + r.first = fYMin; // min y-value + + r.second += fYMax; // accumulative max y-value. + } + } + } + rStore.swap(aStore); + } + + SeriesMinMaxType* getByXValue(double fX) + { + GroupMinMaxType::iterator it = m_SeriesGroup.find(fX); + if (it == m_SeriesGroup.end()) + { + std::pair<GroupMinMaxType::iterator,bool> r = + m_SeriesGroup.insert(std::make_pair(fX, SeriesMinMaxType{})); + + if (!r.second) + // insertion failed. + return nullptr; + + it = r.first; + } + + return &it->second; + } +}; + +} + +void VDataSeriesGroup::getMinimumAndMaximumYInContinuousXRange( + double& rfMinY, double& rfMaxY, double fMinX, double fMaxX, sal_Int32 nAxisIndex ) const +{ + rfMinY = std::numeric_limits<double>::quiet_NaN(); + rfMaxY = std::numeric_limits<double>::quiet_NaN(); + + if (m_aSeriesVector.empty()) + // No data series. Bail out. + return; + + PerXMinMaxCalculator aRangeCalc; + for (const std::unique_ptr<VDataSeries> & pSeries : m_aSeriesVector) + { + if (!pSeries) + continue; + + for (sal_Int32 i = 0, n = pSeries->getTotalPointCount(); i < n; ++i) + { + if (nAxisIndex != pSeries->getAttachedAxisIndex()) + continue; + + double fX = pSeries->getXValue(i); + if (std::isnan(fX)) + continue; + + if (fX < fMinX || fX > fMaxX) + // Outside specified X range. Skip it. + continue; + + double fY = pSeries->getYValue(i); + if (std::isnan(fY)) + continue; + + aRangeCalc.setValue(fX, fY); + } + aRangeCalc.nextSeries(); + } + + aRangeCalc.getTotalRange(rfMinY, rfMaxY); +} + +void VDataSeriesGroup::calculateYMinAndMaxForCategory( sal_Int32 nCategoryIndex + , bool bSeparateStackingForDifferentSigns + , double& rfMinimumY, double& rfMaximumY, sal_Int32 nAxisIndex ) const +{ + assert(nCategoryIndex >= 0); + assert(nCategoryIndex < getPointCount()); + + rfMinimumY = std::numeric_limits<double>::infinity(); + rfMaximumY = -std::numeric_limits<double>::infinity(); + + if(m_aSeriesVector.empty()) + return; + + CachedYValues aCachedYValues = m_aListOfCachedYValues[nCategoryIndex][nAxisIndex]; + if( !aCachedYValues.m_bValuesDirty ) + { + //return cached values + rfMinimumY = aCachedYValues.m_fMinimumY; + rfMaximumY = aCachedYValues.m_fMaximumY; + return; + } + + double fTotalSum = std::numeric_limits<double>::quiet_NaN(); + double fPositiveSum = std::numeric_limits<double>::quiet_NaN(); + double fNegativeSum = std::numeric_limits<double>::quiet_NaN(); + double fFirstPositiveY = std::numeric_limits<double>::quiet_NaN(); + double fFirstNegativeY = std::numeric_limits<double>::quiet_NaN(); + + if( bSeparateStackingForDifferentSigns ) + { + for (const std::unique_ptr<VDataSeries> & pSeries: m_aSeriesVector) + { + if( nAxisIndex != pSeries->getAttachedAxisIndex() ) + continue; + + double fValueMinY = pSeries->getMinimumofAllDifferentYValues( nCategoryIndex ); + double fValueMaxY = pSeries->getMaximumofAllDifferentYValues( nCategoryIndex ); + + if( fValueMaxY >= 0 ) + { + if( std::isnan( fPositiveSum ) ) + fPositiveSum = fFirstPositiveY = fValueMaxY; + else + fPositiveSum += fValueMaxY; + } + if( fValueMinY < 0 ) + { + if(std::isnan( fNegativeSum )) + fNegativeSum = fFirstNegativeY = fValueMinY; + else + fNegativeSum += fValueMinY; + } + } + rfMinimumY = std::isnan( fNegativeSum ) ? fFirstPositiveY : fNegativeSum; + rfMaximumY = std::isnan( fPositiveSum ) ? fFirstNegativeY : fPositiveSum; + } + else + { + for (const std::unique_ptr<VDataSeries> & pSeries: m_aSeriesVector) + { + if( nAxisIndex != pSeries->getAttachedAxisIndex() ) + continue; + + double fValueMinY = pSeries->getMinimumofAllDifferentYValues( nCategoryIndex ); + double fValueMaxY = pSeries->getMaximumofAllDifferentYValues( nCategoryIndex ); + + if( std::isnan( fTotalSum ) ) + { + rfMinimumY = fValueMinY; + rfMaximumY = fTotalSum = fValueMaxY; + } + else + { + fTotalSum += fValueMaxY; + if( rfMinimumY > fTotalSum ) + rfMinimumY = fTotalSum; + if( rfMaximumY < fTotalSum ) + rfMaximumY = fTotalSum; + } + } + } + + aCachedYValues.m_fMinimumY = rfMinimumY; + aCachedYValues.m_fMaximumY = rfMaximumY; + aCachedYValues.m_bValuesDirty = false; + m_aListOfCachedYValues[nCategoryIndex][nAxisIndex]=aCachedYValues; +} + +void VDataSeriesGroup::calculateYMinAndMaxForCategoryRange( + sal_Int32 nStartCategoryIndex, sal_Int32 nEndCategoryIndex + , bool bSeparateStackingForDifferentSigns + , double& rfMinimumY, double& rfMaximumY, sal_Int32 nAxisIndex ) +{ + //@todo maybe cache these values + rfMinimumY = std::numeric_limits<double>::infinity(); + rfMaximumY = -std::numeric_limits<double>::infinity(); + + //iterate through the given categories + if(nStartCategoryIndex<0) + nStartCategoryIndex=0; + const sal_Int32 nPointCount = getPointCount();//necessary to create m_aListOfCachedYValues + if(nPointCount <= 0) + return; + if (nEndCategoryIndex >= nPointCount) + nEndCategoryIndex = nPointCount - 1; + if(nEndCategoryIndex<0) + nEndCategoryIndex=0; + for( sal_Int32 nCatIndex = nStartCategoryIndex; nCatIndex <= nEndCategoryIndex; nCatIndex++ ) + { + double fMinimumY = std::numeric_limits<double>::quiet_NaN(); + double fMaximumY = std::numeric_limits<double>::quiet_NaN(); + + calculateYMinAndMaxForCategory( nCatIndex + , bSeparateStackingForDifferentSigns, fMinimumY, fMaximumY, nAxisIndex ); + + if(rfMinimumY > fMinimumY) + rfMinimumY = fMinimumY; + if(rfMaximumY < fMaximumY) + rfMaximumY = fMaximumY; + } +} + +double VSeriesPlotter::getTransformedDepth() const +{ + double MinZ = m_pMainPosHelper->getLogicMinZ(); + double MaxZ = m_pMainPosHelper->getLogicMaxZ(); + m_pMainPosHelper->doLogicScaling( nullptr, nullptr, &MinZ ); + m_pMainPosHelper->doLogicScaling( nullptr, nullptr, &MaxZ ); + return FIXED_SIZE_FOR_3D_CHART_VOLUME/(MaxZ-MinZ); +} + +void VSeriesPlotter::addSecondaryValueScale( const ExplicitScaleData& rScale, sal_Int32 nAxisIndex ) +{ + if( nAxisIndex<1 ) + return; + + m_aSecondaryValueScales[nAxisIndex]=rScale; +} + +PlottingPositionHelper& VSeriesPlotter::getPlottingPositionHelper( sal_Int32 nAxisIndex ) const +{ + PlottingPositionHelper* pRet = nullptr; + if(nAxisIndex>0) + { + tSecondaryPosHelperMap::const_iterator aPosIt = m_aSecondaryPosHelperMap.find( nAxisIndex ); + if( aPosIt != m_aSecondaryPosHelperMap.end() ) + { + pRet = aPosIt->second.get(); + } + else if (m_pPosHelper) + { + tSecondaryValueScales::const_iterator aScaleIt = m_aSecondaryValueScales.find( nAxisIndex ); + if( aScaleIt != m_aSecondaryValueScales.end() ) + { + m_aSecondaryPosHelperMap[nAxisIndex] = m_pPosHelper->createSecondaryPosHelper( aScaleIt->second ); + pRet = m_aSecondaryPosHelperMap[nAxisIndex].get(); + } + } + } + if( !pRet ) + pRet = m_pMainPosHelper; + pRet->setTimeResolution( m_nTimeResolution, m_aNullDate ); + return *pRet; +} + +void VSeriesPlotter::rearrangeLabelToAvoidOverlapIfRequested( const awt::Size& /*rPageSize*/ ) +{ +} + +VDataSeries* VSeriesPlotter::getFirstSeries() const +{ + for (std::vector<VDataSeriesGroup> const & rGroup : m_aZSlots) + { + if (!rGroup.empty()) + { + if (!rGroup[0].m_aSeriesVector.empty()) + { + VDataSeries* pSeries = rGroup[0].m_aSeriesVector[0].get(); + if (pSeries) + return pSeries; + } + } + } + return nullptr; +} + +OUString VSeriesPlotter::getCategoryName( sal_Int32 nPointIndex ) const +{ + if (m_pExplicitCategoriesProvider) + { + Sequence< OUString > aCategories(m_pExplicitCategoriesProvider->getSimpleCategories()); + if (nPointIndex >= 0 && nPointIndex < aCategories.getLength()) + { + return aCategories[nPointIndex]; + } + } + return OUString(); +} + +std::vector<VDataSeries const*> VSeriesPlotter::getAllSeries() const +{ + std::vector<VDataSeries const*> aAllSeries; + for (std::vector<VDataSeriesGroup> const & rXSlot : m_aZSlots) + { + for(VDataSeriesGroup const & rGroup : rXSlot) + { + for (std::unique_ptr<VDataSeries> const & p : rGroup.m_aSeriesVector) + aAllSeries.push_back(p.get()); + } + } + return aAllSeries; +} + +std::vector<VDataSeries*> VSeriesPlotter::getAllSeries() +{ + std::vector<VDataSeries*> aAllSeries; + for (std::vector<VDataSeriesGroup> const & rXSlot : m_aZSlots) + { + for(VDataSeriesGroup const & rGroup : rXSlot) + { + for (std::unique_ptr<VDataSeries> const & p : rGroup.m_aSeriesVector) + aAllSeries.push_back(p.get()); + } + } + return aAllSeries; +} + +uno::Sequence<OUString> VSeriesPlotter::getSeriesNames() const +{ + std::vector<OUString> aRetVector; + + OUString aRole; + if (m_xChartTypeModel.is()) + aRole = m_xChartTypeModel->getRoleOfSequenceForSeriesLabel(); + + for (auto const& rGroup : m_aZSlots) + { + if (!rGroup.empty()) + { + VDataSeriesGroup const & rSeriesGroup(rGroup[0]); + if (!rSeriesGroup.m_aSeriesVector.empty()) + { + VDataSeries const * pSeries = rSeriesGroup.m_aSeriesVector[0].get(); + rtl::Reference< DataSeries > xSeries( pSeries ? pSeries->getModel() : nullptr ); + if( xSeries.is() ) + { + OUString aSeriesName( DataSeriesHelper::getDataSeriesLabel( xSeries, aRole ) ); + aRetVector.push_back( aSeriesName ); + } + } + } + } + return comphelper::containerToSequence( aRetVector ); +} + +void VSeriesPlotter::setPageReferenceSize( const css::awt::Size & rPageRefSize ) +{ + m_aPageReferenceSize = rPageRefSize; + + // set reference size also at all data series + + for (auto const & outer : m_aZSlots) + for (VDataSeriesGroup const & rGroup : outer) + { + for (std::unique_ptr<VDataSeries> const & pSeries : rGroup.m_aSeriesVector) + { + pSeries->setPageReferenceSize(m_aPageReferenceSize); + } + } +} + +//better performance for big data +void VSeriesPlotter::setCoordinateSystemResolution( const Sequence< sal_Int32 >& rCoordinateSystemResolution ) +{ + m_aCoordinateSystemResolution = rCoordinateSystemResolution; +} + +bool VSeriesPlotter::WantToPlotInFrontOfAxisLine() +{ + return ChartTypeHelper::isSeriesInFrontOfAxisLine( m_xChartTypeModel ); +} + +bool VSeriesPlotter::shouldSnapRectToUsedArea() +{ + return m_nDimension != 3; +} + +std::vector< ViewLegendEntry > VSeriesPlotter::createLegendEntries( + const awt::Size& rEntryKeyAspectRatio + , LegendPosition eLegendPosition + , const Reference< beans::XPropertySet >& xTextProperties + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , const Reference< uno::XComponentContext >& xContext + , ChartModel& rModel + ) +{ + std::vector< ViewLegendEntry > aResult; + + if( xTarget.is() ) + { + rtl::Reference< Diagram > xDiagram = rModel.getFirstChartDiagram(); + rtl::Reference< BaseCoordinateSystem > xCooSys(xDiagram->getBaseCoordinateSystems()[0]); + bool bSwapXAndY = false; + + try + { + xCooSys->getPropertyValue( "SwapXAndYAxis" ) >>= bSwapXAndY; + } + catch( const uno::Exception& ) + { + } + + //iterate through all series + bool bBreak = false; + bool bFirstSeries = true; + + + for (std::vector<VDataSeriesGroup> const & rGroupVector : m_aZSlots) + { + for (VDataSeriesGroup const & rGroup : rGroupVector) + { + for (std::unique_ptr<VDataSeries> const & pSeries : rGroup.m_aSeriesVector) + { + if (!pSeries) + continue; + + if (!pSeries->getPropertiesOfSeries()->getPropertyValue("ShowLegendEntry").get<sal_Bool>()) + { + continue; + } + + std::vector<ViewLegendEntry> aSeriesEntries( + createLegendEntriesForSeries( + rEntryKeyAspectRatio, *pSeries, xTextProperties, + xTarget, xContext)); + + //add series entries to the result now + + // use only the first series if VaryColorsByPoint is set for the first series + if (bFirstSeries && pSeries->isVaryColorsByPoint()) + bBreak = true; + bFirstSeries = false; + + // add entries reverse if chart is stacked in y-direction and the legend position is right or left. + // If the legend is top or bottom and we have a stacked bar-chart the normal order + // is the correct one, unless the chart type is horizontal bar-chart. + bool bReverse = false; + if ( bSwapXAndY ) + { + StackingDirection eStackingDirection( pSeries->getStackingDirection() ); + bReverse = ( eStackingDirection != StackingDirection_Y_STACKING ); + } + else if ( eLegendPosition == LegendPosition_LINE_START || eLegendPosition == LegendPosition_LINE_END ) + { + StackingDirection eStackingDirection( pSeries->getStackingDirection() ); + bReverse = ( eStackingDirection == StackingDirection_Y_STACKING ); + } + + if (bReverse) + aResult.insert( aResult.begin(), aSeriesEntries.begin(), aSeriesEntries.end() ); + else + aResult.insert( aResult.end(), aSeriesEntries.begin(), aSeriesEntries.end() ); + } + if (bBreak) + return aResult; + } + } + } + + return aResult; +} + +namespace +{ +bool lcl_HasVisibleLine( const uno::Reference< beans::XPropertySet >& xProps, bool& rbHasDashedLine ) +{ + bool bHasVisibleLine = false; + rbHasDashedLine = false; + drawing::LineStyle aLineStyle = drawing::LineStyle_NONE; + if( xProps.is() && ( xProps->getPropertyValue( "LineStyle") >>= aLineStyle ) ) + { + if( aLineStyle != drawing::LineStyle_NONE ) + bHasVisibleLine = true; + if( aLineStyle == drawing::LineStyle_DASH ) + rbHasDashedLine = true; + } + return bHasVisibleLine; +} + +bool lcl_HasRegressionCurves( const VDataSeries& rSeries, bool& rbHasDashedLine ) +{ + bool bHasRegressionCurves = false; + rtl::Reference< DataSeries > xRegrCont( rSeries.getModel() ); + for( const rtl::Reference< RegressionCurveModel > & rCurve : xRegrCont->getRegressionCurves2() ) + { + bHasRegressionCurves = true; + lcl_HasVisibleLine( rCurve, rbHasDashedLine ); + } + return bHasRegressionCurves; +} +} +LegendSymbolStyle VSeriesPlotter::getLegendSymbolStyle() +{ + return LegendSymbolStyle::Box; +} + +awt::Size VSeriesPlotter::getPreferredLegendKeyAspectRatio() +{ + awt::Size aRet(1000,1000); + if( m_nDimension==3 ) + return aRet; + + bool bSeriesAllowsLines = (getLegendSymbolStyle() == LegendSymbolStyle::Line); + bool bHasLines = false; + bool bHasDashedLines = false; + //iterate through all series + for (VDataSeries* pSeries : getAllSeries()) + { + if( bSeriesAllowsLines ) + { + bool bCurrentDashed = false; + if( lcl_HasVisibleLine( pSeries->getPropertiesOfSeries(), bCurrentDashed ) ) + { + bHasLines = true; + if( bCurrentDashed ) + { + bHasDashedLines = true; + break; + } + } + } + bool bRegressionHasDashedLines=false; + if( lcl_HasRegressionCurves( *pSeries, bRegressionHasDashedLines ) ) + { + bHasLines = true; + if( bRegressionHasDashedLines ) + { + bHasDashedLines = true; + break; + } + } + } + if( bHasLines ) + { + if( bHasDashedLines ) + aRet = awt::Size(1600,-1); + else + aRet = awt::Size(800,-1); + } + return aRet; +} + +uno::Any VSeriesPlotter::getExplicitSymbol( const VDataSeries& /*rSeries*/, sal_Int32 /*nPointIndex*/ ) +{ + return uno::Any(); +} + +rtl::Reference<SvxShapeGroup> VSeriesPlotter::createLegendSymbolForSeries( + const awt::Size& rEntryKeyAspectRatio + , const VDataSeries& rSeries + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + + LegendSymbolStyle eLegendSymbolStyle = getLegendSymbolStyle(); + uno::Any aExplicitSymbol( getExplicitSymbol( rSeries, -1 ) ); + + VLegendSymbolFactory::PropertyType ePropType = + VLegendSymbolFactory::PropertyType::FilledSeries; + + // todo: maybe the property-style does not solely depend on the + // legend-symbol type + switch( eLegendSymbolStyle ) + { + case LegendSymbolStyle::Line: + ePropType = VLegendSymbolFactory::PropertyType::LineSeries; + break; + default: + break; + } + rtl::Reference<SvxShapeGroup> xShape = VLegendSymbolFactory::createSymbol( rEntryKeyAspectRatio, + xTarget, eLegendSymbolStyle, + rSeries.getPropertiesOfSeries(), ePropType, aExplicitSymbol ); + + return xShape; +} + +rtl::Reference< SvxShapeGroup > VSeriesPlotter::createLegendSymbolForPoint( + const awt::Size& rEntryKeyAspectRatio + , const VDataSeries& rSeries + , sal_Int32 nPointIndex + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget ) +{ + + LegendSymbolStyle eLegendSymbolStyle = getLegendSymbolStyle(); + uno::Any aExplicitSymbol( getExplicitSymbol(rSeries,nPointIndex) ); + + VLegendSymbolFactory::PropertyType ePropType = + VLegendSymbolFactory::PropertyType::FilledSeries; + + // todo: maybe the property-style does not solely depend on the + // legend-symbol type + switch( eLegendSymbolStyle ) + { + case LegendSymbolStyle::Line: + ePropType = VLegendSymbolFactory::PropertyType::LineSeries; + break; + default: + break; + } + + // the default properties for the data point are the data series properties. + // If a data point has own attributes overwrite them + Reference< beans::XPropertySet > xSeriesProps( rSeries.getPropertiesOfSeries() ); + Reference< beans::XPropertySet > xPointSet( xSeriesProps ); + if( rSeries.isAttributedDataPoint( nPointIndex ) ) + xPointSet.set( rSeries.getPropertiesOfPoint( nPointIndex )); + + // if a data point has no own color use a color from the diagram's color scheme + if( ! rSeries.hasPointOwnColor( nPointIndex )) + { + Reference< util::XCloneable > xCloneable( xPointSet,uno::UNO_QUERY ); + if( xCloneable.is() && m_xColorScheme.is() ) + { + xPointSet.set( xCloneable->createClone(), uno::UNO_QUERY ); + Reference< container::XChild > xChild( xPointSet, uno::UNO_QUERY ); + if( xChild.is()) + xChild->setParent( xSeriesProps ); + + OSL_ASSERT( xPointSet.is()); + xPointSet->setPropertyValue( + "Color", uno::Any( m_xColorScheme->getColorByIndex( nPointIndex ))); + } + } + + rtl::Reference< SvxShapeGroup > xShape = VLegendSymbolFactory::createSymbol( rEntryKeyAspectRatio, + xTarget, eLegendSymbolStyle, xPointSet, ePropType, aExplicitSymbol ); + + return xShape; +} + +std::vector< ViewLegendEntry > VSeriesPlotter::createLegendEntriesForSeries( + const awt::Size& rEntryKeyAspectRatio + , const VDataSeries& rSeries + , const Reference< beans::XPropertySet >& xTextProperties + , const rtl::Reference<SvxShapeGroupAnyD>& xTarget + , const Reference< uno::XComponentContext >& xContext + ) +{ + std::vector< ViewLegendEntry > aResult; + + if( ! ( xTarget.is() && xContext.is() ) ) + return aResult; + + try + { + ViewLegendEntry aEntry; + OUString aLabelText; + bool bVaryColorsByPoint = rSeries.isVaryColorsByPoint(); + bool bIsPie = m_xChartTypeModel->getChartType().equalsIgnoreAsciiCase( + CHART2_SERVICE_NAME_CHARTTYPE_PIE); + try + { + if (bIsPie) + { + bool bDonut = false; + if ((m_xChartTypeModel->getPropertyValue("UseRings") >>= bDonut) && bDonut) + bIsPie = false; + } + } + catch (const uno::Exception&) + { + } + + if (bVaryColorsByPoint || bIsPie) + { + Sequence< OUString > aCategoryNames; + if( m_pExplicitCategoriesProvider ) + aCategoryNames = m_pExplicitCategoriesProvider->getSimpleCategories(); + Sequence<sal_Int32> deletedLegendEntries; + try + { + rSeries.getPropertiesOfSeries()->getPropertyValue("DeletedLegendEntries") >>= deletedLegendEntries; + } + catch (const uno::Exception&) + { + } + for( sal_Int32 nIdx=0; nIdx<aCategoryNames.getLength(); ++nIdx ) + { + bool deletedLegendEntry = false; + for (const auto& deletedLegendEntryIdx : std::as_const(deletedLegendEntries)) + { + if (nIdx == deletedLegendEntryIdx) + { + deletedLegendEntry = true; + break; + } + } + if (deletedLegendEntry) + continue; + + // symbol + rtl::Reference< SvxShapeGroup > xSymbolGroup(ShapeFactory::createGroup2D( xTarget )); + + // create the symbol + rtl::Reference< SvxShapeGroup > xShape = createLegendSymbolForPoint( rEntryKeyAspectRatio, + rSeries, nIdx, xSymbolGroup ); + + // set CID to symbol for selection + if( xShape.is() ) + { + aEntry.xSymbol = xSymbolGroup; + + OUString aChildParticle( ObjectIdentifier::createChildParticleWithIndex( OBJECTTYPE_DATA_POINT, nIdx ) ); + aChildParticle = ObjectIdentifier::addChildParticle( aChildParticle, ObjectIdentifier::createChildParticleWithIndex( OBJECTTYPE_LEGEND_ENTRY, 0 ) ); + OUString aCID = ObjectIdentifier::createClassifiedIdentifierForParticles( rSeries.getSeriesParticle(), aChildParticle ); + ShapeFactory::setShapeName( xShape, aCID ); + } + + // label + aLabelText = aCategoryNames[nIdx]; + if( xShape.is() || !aLabelText.isEmpty() ) + { + aEntry.aLabel = FormattedStringHelper::createFormattedStringSequence( xContext, aLabelText, xTextProperties ); + aResult.push_back(aEntry); + } + } + } + else + { + // symbol + rtl::Reference< SvxShapeGroup > xSymbolGroup(ShapeFactory::createGroup2D( xTarget )); + + // create the symbol + rtl::Reference<SvxShapeGroup> xShape = createLegendSymbolForSeries( + rEntryKeyAspectRatio, rSeries, xSymbolGroup ); + + // set CID to symbol for selection + if( xShape.is()) + { + aEntry.xSymbol = xSymbolGroup; + + OUString aChildParticle( ObjectIdentifier::createChildParticleWithIndex( OBJECTTYPE_LEGEND_ENTRY, 0 ) ); + OUString aCID = ObjectIdentifier::createClassifiedIdentifierForParticles( rSeries.getSeriesParticle(), aChildParticle ); + ShapeFactory::setShapeName( xShape, aCID ); + } + + // label + aLabelText = DataSeriesHelper::getDataSeriesLabel( rSeries.getModel(), m_xChartTypeModel.is() ? m_xChartTypeModel->getRoleOfSequenceForSeriesLabel() : "values-y"); + aEntry.aLabel = FormattedStringHelper::createFormattedStringSequence( xContext, aLabelText, xTextProperties ); + + aResult.push_back(aEntry); + } + + // don't show legend entry of regression curve & friends if this type of chart + // doesn't support statistics #i63016#, fdo#37197 + if (!ChartTypeHelper::isSupportingStatisticProperties( m_xChartTypeModel, m_nDimension )) + return aResult; + + rtl::Reference< DataSeries > xRegrCont = rSeries.getModel(); + if( xRegrCont.is()) + { + const std::vector< rtl::Reference< RegressionCurveModel > > & aCurves = xRegrCont->getRegressionCurves2(); + sal_Int32 i = 0, nCount = aCurves.size(); + for( i=0; i<nCount; ++i ) + { + //label + OUString aResStr( RegressionCurveHelper::getUINameForRegressionCurve( aCurves[i] ) ); + replaceParamterInString( aResStr, "%SERIESNAME", aLabelText ); + aEntry.aLabel = FormattedStringHelper::createFormattedStringSequence( xContext, aResStr, xTextProperties ); + + // symbol + rtl::Reference<SvxShapeGroup> xSymbolGroup(ShapeFactory::createGroup2D( xTarget )); + + // create the symbol + rtl::Reference<SvxShapeGroup> xShape = VLegendSymbolFactory::createSymbol( rEntryKeyAspectRatio, + xSymbolGroup, LegendSymbolStyle::Line, + aCurves[i], + VLegendSymbolFactory::PropertyType::Line, uno::Any() ); + + // set CID to symbol for selection + if( xShape.is()) + { + aEntry.xSymbol = xSymbolGroup; + + bool bAverageLine = RegressionCurveHelper::isMeanValueLine( aCurves[i] ); + ObjectType eObjectType = bAverageLine ? OBJECTTYPE_DATA_AVERAGE_LINE : OBJECTTYPE_DATA_CURVE; + OUString aChildParticle( ObjectIdentifier::createChildParticleWithIndex( eObjectType, i ) ); + aChildParticle = ObjectIdentifier::addChildParticle( aChildParticle, ObjectIdentifier::createChildParticleWithIndex( OBJECTTYPE_LEGEND_ENTRY, 0 ) ); + OUString aCID = ObjectIdentifier::createClassifiedIdentifierForParticles( rSeries.getSeriesParticle(), aChildParticle ); + ShapeFactory::setShapeName( xShape, aCID ); + } + + aResult.push_back(aEntry); + } + } + } + catch( const uno::Exception & ) + { + DBG_UNHANDLED_EXCEPTION("chart2" ); + } + return aResult; +} + +VSeriesPlotter* VSeriesPlotter::createSeriesPlotter( + const rtl::Reference<ChartType>& xChartTypeModel + , sal_Int32 nDimensionCount + , bool bExcludingPositioning ) +{ + if (!xChartTypeModel.is()) + return nullptr; + + OUString aChartType = xChartTypeModel->getChartType(); + + VSeriesPlotter* pRet=nullptr; + if( aChartType.equalsIgnoreAsciiCase( CHART2_SERVICE_NAME_CHARTTYPE_COLUMN ) ) + pRet = new BarChart(xChartTypeModel,nDimensionCount); + else if( aChartType.equalsIgnoreAsciiCase( CHART2_SERVICE_NAME_CHARTTYPE_BAR ) ) + pRet = new BarChart(xChartTypeModel,nDimensionCount); + else if( aChartType.equalsIgnoreAsciiCase( CHART2_SERVICE_NAME_CHARTTYPE_AREA ) ) + pRet = new AreaChart(xChartTypeModel,nDimensionCount,true); + else if( aChartType.equalsIgnoreAsciiCase( CHART2_SERVICE_NAME_CHARTTYPE_LINE ) ) + pRet = new AreaChart(xChartTypeModel,nDimensionCount,true,true); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_SCATTER) ) + pRet = new AreaChart(xChartTypeModel,nDimensionCount,false,true); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_BUBBLE) ) + pRet = new BubbleChart(xChartTypeModel,nDimensionCount); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_PIE) ) + pRet = new PieChart(xChartTypeModel,nDimensionCount, bExcludingPositioning ); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_NET) ) + pRet = new NetChart(xChartTypeModel,nDimensionCount,true,std::make_unique<PolarPlottingPositionHelper>()); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_FILLED_NET) ) + pRet = new NetChart(xChartTypeModel,nDimensionCount,false,std::make_unique<PolarPlottingPositionHelper>()); + else if( aChartType.equalsIgnoreAsciiCase(CHART2_SERVICE_NAME_CHARTTYPE_CANDLESTICK) ) + pRet = new CandleStickChart(xChartTypeModel,nDimensionCount); + else + pRet = new AreaChart(xChartTypeModel,nDimensionCount,false,true); + return pRet; +} + +} //namespace chart + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |