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