xref: /core/oox/source/drawingml/chart/axisconverter.cxx (revision 1180b3473aed3251b5869256abc9c7774bab9ced)
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include <drawingml/chart/axisconverter.hxx>
21 #include <ooxresid.hxx>
22 #include <strings.hrc>
23 
24 #include <com/sun/star/chart/ChartAxisArrangeOrderType.hpp>
25 #include <com/sun/star/chart/ChartAxisLabelPosition.hpp>
26 #include <com/sun/star/chart/ChartAxisMarkPosition.hpp>
27 #include <com/sun/star/chart/ChartAxisPosition.hpp>
28 #include <com/sun/star/chart/TimeInterval.hpp>
29 #include <com/sun/star/chart/TimeUnit.hpp>
30 #include <com/sun/star/chart2/AxisType.hpp>
31 #include <com/sun/star/chart2/TickmarkStyle.hpp>
32 #include <com/sun/star/chart2/LinearScaling.hpp>
33 #include <com/sun/star/chart2/LogarithmicScaling.hpp>
34 #include <com/sun/star/chart2/XAxis.hpp>
35 #include <com/sun/star/chart2/XCoordinateSystem.hpp>
36 #include <com/sun/star/chart2/XTitled.hpp>
37 #include <drawingml/chart/axismodel.hxx>
38 #include <drawingml/chart/titleconverter.hxx>
39 #include <drawingml/chart/typegroupconverter.hxx>
40 #include <drawingml/lineproperties.hxx>
41 #include <drawingml/textbody.hxx>
42 #include <oox/token/namespaces.hxx>
43 #include <oox/token/properties.hxx>
44 #include <oox/token/tokens.hxx>
45 #include <comphelper/processfactory.hxx>
46 #include <osl/diagnose.h>
47 
48 namespace oox::drawingml::chart {
49 
50 using namespace ::com::sun::star::beans;
51 using namespace ::com::sun::star::chart2;
52 using namespace ::com::sun::star::uno;
53 
54 namespace {
55 
lclSetValueOrClearAny(Any & orAny,const std::optional<double> & rofValue)56 void lclSetValueOrClearAny( Any& orAny, const std::optional< double >& rofValue )
57 {
58     if( rofValue.has_value() ) orAny <<= rofValue.value(); else orAny.clear();
59 }
60 
lclIsLogarithmicScale(const AxisModel & rAxisModel)61 bool lclIsLogarithmicScale( const AxisModel& rAxisModel )
62 {
63     return rAxisModel.mofLogBase.has_value() && (2.0 <= rAxisModel.mofLogBase.value()) && (rAxisModel.mofLogBase.value() <= 1000.0);
64 }
65 
lclGetApiTimeUnit(sal_Int32 nTimeUnit)66 sal_Int32 lclGetApiTimeUnit( sal_Int32 nTimeUnit )
67 {
68     using namespace ::com::sun::star::chart;
69     switch( nTimeUnit )
70     {
71         case XML_days:      return TimeUnit::DAY;
72         case XML_months:    return TimeUnit::MONTH;
73         case XML_years:     return TimeUnit::YEAR;
74         default:            OSL_ENSURE( false, "lclGetApiTimeUnit - unexpected time unit" );
75     }
76     return TimeUnit::DAY;
77 }
78 
lclConvertTimeInterval(Any & orInterval,const std::optional<double> & rofUnit,sal_Int32 nTimeUnit)79 void lclConvertTimeInterval( Any& orInterval, const std::optional< double >& rofUnit, sal_Int32 nTimeUnit )
80 {
81     if( rofUnit.has_value() && (1.0 <= rofUnit.value()) && (rofUnit.value() <= SAL_MAX_INT32) )
82         orInterval <<= css::chart::TimeInterval( static_cast< sal_Int32 >( rofUnit.value() ), lclGetApiTimeUnit( nTimeUnit ) );
83     else
84         orInterval.clear();
85 }
86 
lclGetLabelPosition(sal_Int32 nToken)87 css::chart::ChartAxisLabelPosition lclGetLabelPosition( sal_Int32 nToken )
88 {
89     using namespace ::com::sun::star::chart;
90     switch( nToken )
91     {
92         case XML_high:      return ChartAxisLabelPosition_OUTSIDE_END;
93         case XML_low:       return ChartAxisLabelPosition_OUTSIDE_START;
94         case XML_nextTo:    return ChartAxisLabelPosition_NEAR_AXIS;
95     }
96     return ChartAxisLabelPosition_NEAR_AXIS;
97 }
98 
lclGetTickMark(sal_Int32 nToken)99 sal_Int32 lclGetTickMark( sal_Int32 nToken )
100 {
101     using namespace ::com::sun::star::chart2::TickmarkStyle;
102     switch( nToken )
103     {
104         case XML_in:    return INNER;
105         case XML_out:   return OUTER;
106         case XML_cross: return INNER | OUTER;
107     }
108     return css::chart2::TickmarkStyle::NONE;
109 }
110 
111 /**
112  * The groups is of percent type only when all of its members are of percent
113  * type.
114  */
isPercent(const RefVector<TypeGroupConverter> & rTypeGroups)115 bool isPercent( const RefVector<TypeGroupConverter>& rTypeGroups )
116 {
117     if (rTypeGroups.empty())
118         return false;
119 
120     for (auto const& typeGroup : rTypeGroups)
121     {
122         TypeGroupConverter& rConv = *typeGroup;
123         if (!rConv.isPercent())
124             return false;
125     }
126 
127     return true;
128 }
129 
130 } // namespace
131 
AxisConverter(const ConverterRoot & rParent,AxisModel & rModel)132 AxisConverter::AxisConverter( const ConverterRoot& rParent, AxisModel& rModel ) :
133     ConverterBase< AxisModel >( rParent, rModel )
134 {
135 }
136 
~AxisConverter()137 AxisConverter::~AxisConverter()
138 {
139 }
140 
convertFromModel(const Reference<XCoordinateSystem> & rxCoordSystem,RefVector<TypeGroupConverter> & rTypeGroups,const AxisModel * pCrossingAxis,sal_Int32 nAxesSetIdx,sal_Int32 nAxisIdx,bool bUseFixedInnerSize)141 void AxisConverter::convertFromModel(const Reference<XCoordinateSystem>& rxCoordSystem,
142                                      RefVector<TypeGroupConverter>& rTypeGroups,
143                                      const AxisModel* pCrossingAxis, sal_Int32 nAxesSetIdx,
144                                      sal_Int32 nAxisIdx, bool bUseFixedInnerSize)
145 {
146     if (rTypeGroups.empty())
147         return;
148 
149     Reference< XAxis > xAxis;
150     try
151     {
152         namespace cssc = css::chart;
153         namespace cssc2 = css::chart2;
154 
155         const TypeGroupInfo& rTypeInfo = rTypeGroups.front()->getTypeInfo();
156         ObjectFormatter& rFormatter = getFormatter();
157 
158         // create the axis object (always)
159         xAxis.set( createInstance( u"com.sun.star.chart2.Axis"_ustr ), UNO_QUERY_THROW );
160         PropertySet aAxisProp( xAxis );
161         // #i58688# axis enabled
162         aAxisProp.setProperty( PROP_Show, !mrModel.mbDeleted );
163 
164         // axis line, tick, and gridline properties ---------------------------
165 
166         // show axis labels
167         aAxisProp.setProperty( PROP_DisplayLabels, mrModel.mnTickLabelPos != XML_none );
168         aAxisProp.setProperty( PROP_LabelPosition, lclGetLabelPosition( mrModel.mnTickLabelPos ) );
169         // no X axis line in radar charts
170         if( (nAxisIdx == API_X_AXIS) && (rTypeInfo.meTypeCategory == TYPECATEGORY_RADAR) )
171             mrModel.mxShapeProp.getOrCreate().getLineProperties().maLineFill.moFillType = XML_noFill;
172         // axis line and tick label formatting
173         rFormatter.convertFormatting( aAxisProp, mrModel.mxShapeProp, mrModel.mxTextProp, OBJECTTYPE_AXIS );
174         // tick label rotation
175         ObjectFormatter::convertTextRotation( aAxisProp, mrModel.mxTextProp, true );
176 
177         // tick mark style
178         aAxisProp.setProperty( PROP_MajorTickmarks, lclGetTickMark( mrModel.mnMajorTickMark ) );
179         aAxisProp.setProperty( PROP_MinorTickmarks, lclGetTickMark( mrModel.mnMinorTickMark ) );
180         aAxisProp.setProperty( PROP_MarkPosition, cssc::ChartAxisMarkPosition_AT_AXIS );
181 
182         // main grid
183         PropertySet aGridProp( xAxis->getGridProperties() );
184         aGridProp.setProperty( PROP_Show, mrModel.mxMajorGridLines.is() );
185         if( mrModel.mxMajorGridLines.is() )
186             rFormatter.convertFrameFormatting( aGridProp, mrModel.mxMajorGridLines, OBJECTTYPE_MAJORGRIDLINE );
187 
188         // sub grid
189         Sequence< Reference< XPropertySet > > aSubGridPropSeq = xAxis->getSubGridProperties();
190         if( aSubGridPropSeq.hasElements() )
191         {
192             PropertySet aSubGridProp( aSubGridPropSeq[ 0 ] );
193             aSubGridProp.setProperty( PROP_Show, mrModel.mxMinorGridLines.is() );
194             if( mrModel.mxMinorGridLines.is() )
195                 rFormatter.convertFrameFormatting( aSubGridProp, mrModel.mxMinorGridLines, OBJECTTYPE_MINORGRIDLINE );
196         }
197 
198         // axis type and X axis categories ------------------------------------
199 
200         ScaleData aScaleData = xAxis->getScaleData();
201         // set axis type
202         switch( nAxisIdx )
203         {
204             case API_X_AXIS:
205                 if( rTypeInfo.mbCategoryAxis )
206                 {
207                     OSL_ENSURE( (mrModel.mnTypeId == C_TOKEN( catAx )) || (mrModel.mnTypeId == C_TOKEN( dateAx )),
208                         "AxisConverter::convertFromModel - unexpected axis model type (must: c:catAx or c:dateAx)" );
209                     bool bDateAxis = mrModel.mnTypeId == C_TOKEN( dateAx );
210                     // tdf#132076: set axis type to date, if it is a date axis!
211                     aScaleData.AxisType = bDateAxis ? cssc2::AxisType::DATE : cssc2::AxisType::CATEGORY;
212                     aScaleData.AutoDateAxis = mrModel.mbAuto;
213                     /* TODO: create main category axis labels once, while InternalDataProvider
214                     can not handle different category names on the primary and secondary category axis. */
215                     if( nAxesSetIdx == 0 )
216                         aScaleData.Categories = rTypeGroups.front()->createCategorySequence();
217                     /* set default ShiftedCategoryPosition values for some charttype,
218                        because the XML can contain wrong CrossBetween value, if came from MSO */
219                     if( rTypeGroups.front()->is3dChart() && (rTypeInfo.meTypeId == TYPEID_BAR || rTypeInfo.meTypeId == TYPEID_HORBAR || rTypeInfo.meTypeId == TYPEID_STOCK) )
220                         aScaleData.ShiftedCategoryPosition = true;
221                     else if( rTypeInfo.meTypeId == TYPEID_RADARLINE || rTypeInfo.meTypeId == TYPEID_RADARAREA )
222                         aScaleData.ShiftedCategoryPosition = false;
223                     else if( pCrossingAxis->mnCrossBetween != -1 ) /*because of backwards compatibility*/
224                         aScaleData.ShiftedCategoryPosition = pCrossingAxis->mnCrossBetween == XML_between;
225                     else if( rTypeInfo.meTypeCategory == TYPECATEGORY_BAR || rTypeInfo.meTypeId == TYPEID_LINE || rTypeInfo.meTypeId == TYPEID_STOCK )
226                         aScaleData.ShiftedCategoryPosition = true;
227                 }
228                 else
229                 {
230                     OSL_ENSURE( mrModel.mnTypeId == C_TOKEN( valAx ), "AxisConverter::convertFromModel - unexpected axis model type (must: c:valAx)" );
231                     aScaleData.AxisType = cssc2::AxisType::REALNUMBER;
232                 }
233             break;
234             case API_Y_AXIS:
235                 OSL_ENSURE( mrModel.mnTypeId == C_TOKEN( valAx ), "AxisConverter::convertFromModel - unexpected axis model type (must: c:valAx)" );
236                 aScaleData.AxisType = isPercent(rTypeGroups) ? cssc2::AxisType::PERCENT : cssc2::AxisType::REALNUMBER;
237             break;
238             case API_Z_AXIS:
239                 OSL_ENSURE( mrModel.mnTypeId == C_TOKEN( serAx ), "AxisConverter::convertFromModel - unexpected axis model type (must: c:serAx)" );
240                 OSL_ENSURE( rTypeGroups.front()->isDeep3dChart(), "AxisConverter::convertFromModel - series axis not supported by this chart type" );
241                 aScaleData.AxisType = cssc2::AxisType::SERIES;
242             break;
243         }
244 
245         // axis scaling and increment -----------------------------------------
246 
247         switch( aScaleData.AxisType )
248         {
249             case cssc2::AxisType::CATEGORY:
250             case cssc2::AxisType::SERIES:
251             case cssc2::AxisType::DATE:
252             {
253                 /*  Determine date axis type from XML type identifier, and not
254                     via aScaleData.AxisType, as this value sticks to CATEGORY
255                     for automatic category/date axes). */
256                 if( mrModel.mnTypeId == C_TOKEN( dateAx ) )
257                 {
258                     // scaling algorithm
259                     aScaleData.Scaling = LinearScaling::create( comphelper::getProcessComponentContext() );
260                     // min/max
261                     lclSetValueOrClearAny( aScaleData.Minimum, mrModel.mofMin );
262                     lclSetValueOrClearAny( aScaleData.Maximum, mrModel.mofMax );
263                     // major/minor increment
264                     lclConvertTimeInterval( aScaleData.TimeIncrement.MajorTimeInterval, mrModel.mofMajorUnit, mrModel.mnMajorTimeUnit );
265                     lclConvertTimeInterval( aScaleData.TimeIncrement.MinorTimeInterval, mrModel.mofMinorUnit, mrModel.mnMinorTimeUnit );
266                     // base time unit
267                     if( mrModel.monBaseTimeUnit.has_value() )
268                         aScaleData.TimeIncrement.TimeResolution <<= lclGetApiTimeUnit( mrModel.monBaseTimeUnit.value() );
269                     else
270                         aScaleData.TimeIncrement.TimeResolution.clear();
271                 }
272                 else
273                 {
274                     // do not overlap text unless the rotation is 0 in xml
275                     bool bTextOverlap = false;
276                     if (mrModel.mxTextProp.is()
277                         && mrModel.mxTextProp->getTextProperties().moTextAreaRotation.has_value())
278                         bTextOverlap
279                             = mrModel.mxTextProp->getTextProperties().moTextAreaRotation.value() == 0;
280                     aAxisProp.setProperty(PROP_TextOverlap, bTextOverlap);
281                     /* do not break text into several lines unless the rotation is 0 degree,
282                        or the rotation is 90 degree and the inner size of the chart is not fixed,
283                        or the rotation is 270 degree and the inner size of the chart is not fixed */
284                     bool bTextBreak = true;
285                     double fRotationAngle = 0.0;
286                     if (aAxisProp.getProperty(fRotationAngle, PROP_TextRotation)
287                         && fRotationAngle != 0.0)
288                         bTextBreak = !bUseFixedInnerSize
289                                      && (fRotationAngle == 90.0 || fRotationAngle == 270.0);
290                     aAxisProp.setProperty(PROP_TextBreak, bTextBreak);
291                     // do not stagger labels in two lines
292                     aAxisProp.setProperty( PROP_ArrangeOrder, cssc::ChartAxisArrangeOrderType_SIDE_BY_SIDE );
293                     //! TODO #i58731# show n-th category
294                 }
295             }
296             break;
297             case cssc2::AxisType::REALNUMBER:
298             case cssc2::AxisType::PERCENT:
299             {
300                 // scaling algorithm
301                 const bool bLogScale = lclIsLogarithmicScale( mrModel );
302                 if( bLogScale )
303                     aScaleData.Scaling = LogarithmicScaling::create( comphelper::getProcessComponentContext() );
304                 else
305                     aScaleData.Scaling = LinearScaling::create( comphelper::getProcessComponentContext() );
306                 // min/max
307                 lclSetValueOrClearAny( aScaleData.Minimum, mrModel.mofMin );
308                 lclSetValueOrClearAny( aScaleData.Maximum, mrModel.mofMax );
309                 // major increment
310                 IncrementData& rIncrementData = aScaleData.IncrementData;
311                 if( mrModel.mofMajorUnit.has_value() && aScaleData.Scaling.is() )
312                     rIncrementData.Distance <<= aScaleData.Scaling->doScaling( mrModel.mofMajorUnit.value() );
313                 else
314                     lclSetValueOrClearAny( rIncrementData.Distance, mrModel.mofMajorUnit );
315                 // minor increment
316                 Sequence< SubIncrement >& rSubIncrementSeq = rIncrementData.SubIncrements;
317                 rSubIncrementSeq.realloc( 1 );
318                 Any& rIntervalCount = rSubIncrementSeq.getArray()[ 0 ].IntervalCount;
319                 rIntervalCount.clear();
320                 if( bLogScale )
321                 {
322                     if( mrModel.mofMinorUnit.has_value() )
323                         rIntervalCount <<= sal_Int32( 9 );
324                 }
325                 else if( mrModel.mofMajorUnit.has_value() && mrModel.mofMinorUnit.has_value() && (0.0 < mrModel.mofMinorUnit.value()) && (mrModel.mofMinorUnit.value() <= mrModel.mofMajorUnit.value()) )
326                 {
327                     double fCount = mrModel.mofMajorUnit.value() / mrModel.mofMinorUnit.value() + 0.5;
328                     if( (1.0 <= fCount) && (fCount < 1001.0) )
329                         rIntervalCount <<= static_cast< sal_Int32 >( fCount );
330                 }
331                 else if( !mrModel.mofMinorUnit.has_value() )
332                 {
333                     // tdf#114168 If minor unit is not set then set interval to 5, as MS Excel do.
334                     rIntervalCount <<= static_cast< sal_Int32 >( 5 );
335                 }
336             }
337             break;
338             default:
339                 OSL_FAIL( "AxisConverter::convertFromModel - unknown axis type" );
340         }
341 
342         /*  Do not set a value to the Origin member anymore (already done via
343             new axis properties 'CrossoverPosition' and 'CrossoverValue'). */
344         aScaleData.Origin.clear();
345 
346         // axis orientation ---------------------------------------------------
347 
348         // #i85167# pie/donut charts need opposite direction at Y axis
349         // #i87747# radar charts need opposite direction at X axis
350         bool bMirrorDirection =
351             ((nAxisIdx == API_Y_AXIS) && (rTypeInfo.meTypeCategory == TYPECATEGORY_PIE)) ||
352             ((nAxisIdx == API_X_AXIS) && (rTypeInfo.meTypeCategory == TYPECATEGORY_RADAR));
353         bool bReverse = (mrModel.mnOrientation == XML_maxMin) != bMirrorDirection;
354         aScaleData.Orientation = bReverse ? cssc2::AxisOrientation_REVERSE : cssc2::AxisOrientation_MATHEMATICAL;
355 
356         // write back scaling data
357         xAxis->setScaleData( aScaleData );
358 
359         // number format ------------------------------------------------------
360         if( !mrModel.mbDeleted && aScaleData.AxisType != cssc2::AxisType::SERIES )
361         {
362             getFormatter().convertNumberFormat(aAxisProp, mrModel.maNumberFormat, true);
363         }
364 
365         // position of crossing axis ------------------------------------------
366 
367         bool bManualCrossing = mrModel.mofCrossesAt.has_value();
368         cssc::ChartAxisPosition eAxisPos = cssc::ChartAxisPosition_VALUE;
369         if( !bManualCrossing ) switch( mrModel.mnCrossMode )
370         {
371             case XML_min:       eAxisPos = cssc::ChartAxisPosition_START;   break;
372             case XML_max:       eAxisPos = cssc::ChartAxisPosition_END;     break;
373             case XML_autoZero:  eAxisPos = cssc::ChartAxisPosition_ZERO;   break;
374         }
375 
376         aAxisProp.setProperty( PROP_CrossoverPosition, eAxisPos );
377 
378         // calculate automatic origin depending on scaling mode of crossing axis
379         bool bCrossingLogScale = pCrossingAxis && lclIsLogarithmicScale( *pCrossingAxis );
380         double fCrossingPos = bManualCrossing ? mrModel.mofCrossesAt.value() : (bCrossingLogScale ? 1.0 : 0.0);
381         aAxisProp.setProperty( PROP_CrossoverValue, fCrossingPos );
382 
383         // axis title ---------------------------------------------------------
384 
385         // in radar charts, title objects may exist, but are not shown
386         if( mrModel.mxTitle.is() && (rTypeGroups.front()->getTypeInfo().meTypeCategory != TYPECATEGORY_RADAR) )
387         {
388             Reference< XTitled > xTitled( xAxis, UNO_QUERY_THROW );
389             if (((nAxisIdx == API_X_AXIS && rTypeInfo.meTypeId != TYPEID_HORBAR)
390                 || (nAxisIdx == API_Y_AXIS && rTypeInfo.meTypeId == TYPEID_HORBAR))
391                 && (mrModel.mnAxisPos == XML_l || mrModel.mnAxisPos == XML_r))
392                 mrModel.mxTitle->mnDefaultRotation = 0;
393             TitleConverter aTitleConv( *this, *mrModel.mxTitle );
394             aTitleConv.convertFromModel( xTitled, OoxResId(STR_DIAGRAM_AXISTITLE), OBJECTTYPE_AXISTITLE, nAxesSetIdx, nAxisIdx );
395         }
396 
397         // axis data unit label -----------------------------------------------
398         AxisDispUnitsConverter axisDispUnitsConverter (*this, mrModel.mxDispUnits.getOrCreate());
399         axisDispUnitsConverter.convertFromModel(xAxis);
400     }
401     catch( Exception& )
402     {
403     }
404 
405     if( xAxis.is() && rxCoordSystem.is() ) try
406     {
407         // insert axis into coordinate system
408         rxCoordSystem->setAxisByDimension( nAxisIdx, xAxis, nAxesSetIdx );
409     }
410     catch( Exception& )
411     {
412         OSL_FAIL( "AxisConverter::convertFromModel - cannot insert axis into coordinate system" );
413     }
414 }
415 
AxisDispUnitsConverter(const ConverterRoot & rParent,AxisDispUnitsModel & rModel)416 AxisDispUnitsConverter::AxisDispUnitsConverter( const ConverterRoot& rParent, AxisDispUnitsModel& rModel ) :
417     ConverterBase< AxisDispUnitsModel >( rParent, rModel )
418 {
419 }
420 
~AxisDispUnitsConverter()421 AxisDispUnitsConverter::~AxisDispUnitsConverter()
422 {
423 }
424 
convertFromModel(const Reference<XAxis> & rxAxis)425 void AxisDispUnitsConverter::convertFromModel( const Reference< XAxis >& rxAxis )
426 {
427     PropertySet aPropSet( rxAxis );
428     if (!mrModel.mnBuiltInUnit.isEmpty() )
429     {
430         aPropSet.setProperty(PROP_DisplayUnits, true);
431         aPropSet.setProperty( PROP_BuiltInUnit, mrModel.mnBuiltInUnit );
432     }
433 }
434 
435 } // namespace oox::drawingml::chart
436 
437 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
438