QGIS API Documentation 4.3.0-Master (b46232e4f9a)
Loading...
Searching...
No Matches
qgspiechartplot.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsbarchartplot.cpp
3 -------------------
4 begin : June 2025
5 copyright : (C) 2025 by Mathieu
6 email : mathieu at opengis dot ch
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgspiechartplot.h"
19
20#include "qgsapplication.h"
21#include "qgscolorrampimpl.h"
24#include "qgspainting.h"
25#include "qgssymbol.h"
26#include "qgssymbollayer.h"
27#include "qgssymbollayerutils.h"
28#include "qgstextrenderer.h"
30
31#include <QString>
32
33using namespace Qt::StringLiterals;
34
41
42void QgsPieChartPlot::renderContent( QgsRenderContext &context, QgsPlotRenderContext &, const QRectF &plotArea, const QgsPlotData &plotData )
43{
44 if ( mFillSymbols.empty() )
45 {
46 return;
47 }
48
49 const QList<QgsAbstractPlotSeries *> seriesList = plotData.series();
50 if ( seriesList.isEmpty() || plotData.categories().isEmpty() )
51 {
52 return;
53 }
54
55 const QStringList categories = plotData.categories();
56 double maxLabelHeight = 0;
57 switch ( mLabelType )
58 {
60 {
61 for ( const QString &category : categories )
62 {
63 maxLabelHeight = std::max( maxLabelHeight, QgsTextRenderer::textHeight( context, mLabelTextFormat, { category } ) );
64 }
65 break;
66 }
67
69 {
70 QgsNumericFormatContext numericContext;
71 QString text;
72
73 for ( const QgsAbstractPlotSeries *series : seriesList )
74 {
75 if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
76 {
77 const QList<std::pair<double, double>> data = xySeries->data();
78 for ( const std::pair<double, double> &pair : data )
79 {
80 if ( mNumericFormat )
81 {
82 text = mNumericFormat->formatDouble( pair.second, numericContext );
83 }
84 else
85 {
86 text = QString::number( pair.second );
87 }
88 maxLabelHeight = std::max( maxLabelHeight, QgsTextRenderer::textHeight( context, mLabelTextFormat, { text } ) );
89 }
90 }
91 }
92 break;
93 }
94
96 break;
97 }
98
99 QgsExpressionContextScope *chartScope = new QgsExpressionContextScope( u"chart"_s );
100 const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), chartScope );
101
102 QgsScopedQPainterState painterState( context.painter() );
103 context.painter()->setClipRect( plotArea );
104
105 const bool pieStackHorizontal = plotArea.width() >= plotArea.height();
106 const double pieStackCount = seriesList.size();
107 double pieArea = 0;
108 if ( pieStackHorizontal )
109 {
110 pieArea = plotArea.height() * pieStackCount > plotArea.width() ? plotArea.width() / pieStackCount : plotArea.height();
111 }
112 else
113 {
114 pieArea = plotArea.width() * pieStackCount > plotArea.height() ? plotArea.height() / pieStackCount : plotArea.width();
115 }
116
117 QgsNumericFormatContext numericContext;
118 QMap<QString, QColor> categoriesColor;
119 int seriesIndex = 0;
120 for ( const QgsAbstractPlotSeries *series : seriesList )
121 {
122 QgsFillSymbol *symbol = fillSymbolAt( seriesIndex % mFillSymbols.size() );
123 if ( !symbol )
124 {
125 continue;
126 }
127 const QColor symbolColor = symbol->color();
128 symbol->startRender( context );
129 const double pieWidth = pieArea - QgsSymbolLayerUtils::estimateMaxSymbolBleed( symbol, context ) - maxLabelHeight * 3;
130
131 QgsColorRamp *ramp = colorRampAt( seriesIndex % mColorRamps.size() );
132 if ( QgsRandomColorRamp *randomRamp = dynamic_cast<QgsRandomColorRamp *>( ramp ) )
133 {
134 //ramp is a random colors ramp, so inform it of the total number of required colors
135 //this allows the ramp to pregenerate a set of visually distinctive colors
136 randomRamp->setTotalColorCount( categories.size() );
137 }
138
139 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_series_name"_s, series->name(), true ) );
140
141 if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
142 {
143 const QList<std::pair<double, double>> data = xySeries->data();
144 double yTotal = 0;
145 for ( const std::pair<double, double> &pair : data )
146 {
147 if ( !categoriesColor.contains( categories[pair.first] ) )
148 {
149 if ( ramp )
150 {
151 categoriesColor[categories[pair.first]] = ramp->color( pair.first / ( categories.size() - 1 ) );
152 }
153 else
154 {
155 categoriesColor[categories[pair.first]] = symbolColor;
156 }
157 }
158
159 yTotal += pair.second;
160 }
161
162 double ySum = 0;
163 for ( const std::pair<double, double> &pair : data )
164 {
165 QPointF center;
166 if ( pieStackHorizontal )
167 {
168 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
169 }
170 else
171 {
172 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
173 }
174 QRectF boundingBox( center.x() - pieWidth / 2, center.y() - pieWidth / 2, pieWidth, pieWidth );
175
176 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
177 const double degreesForward = pair.second / yTotal * 360;
178
179 QPainterPath path;
180 path.moveTo( center );
181 path.arcTo( boundingBox, -degreesStart, -degreesForward );
182
183 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_category"_s, categories[pair.first], true ) );
184 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_value"_s, pair.second, true ) );
185 symbol->setColor( categoriesColor[categories[pair.first]] );
186 symbol->renderPolygon( path.toFillPolygon(), nullptr, nullptr, context );
187
188 ySum += pair.second;
189 }
190
191 if ( mLabelType != Qgis::PieChartLabelType::NoLabels )
192 {
193 QString text;
194 ySum = 0;
195 for ( const std::pair<double, double> &pair : data )
196 {
197 QPointF center;
198 if ( pieStackHorizontal )
199 {
200 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
201 }
202 else
203 {
204 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
205 }
206
207 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
208 const double degreesForward = pair.second / yTotal * 360;
209 const double degreesMid = ( degreesStart + ( degreesForward / 2 ) );
210
211 const double labelX = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::cos( degreesMid * M_PI / 180 ) + center.x();
212 const double labelY = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::sin( degreesMid * M_PI / 180 ) + center.y();
213 const double labelYAdjustment = degreesMid > 0 && degreesMid <= 180 ? maxLabelHeight / 2 : 0;
214
216 if ( degreesMid < -85 || ( degreesMid > 85 && degreesMid <= 95 ) || degreesMid > 265 )
217 {
218 horizontalAlignment = Qgis::TextHorizontalAlignment::Center;
219 }
220 else if ( degreesMid > 95 && degreesMid <= 265 )
221 {
222 horizontalAlignment = Qgis::TextHorizontalAlignment::Right;
223 }
224
225 switch ( mLabelType )
226 {
228 text = categories[pair.first];
229 break;
230
232 if ( mNumericFormat )
233 {
234 text = mNumericFormat->formatDouble( pair.second, numericContext );
235 }
236 else
237 {
238 text = QString::number( pair.second );
239 }
240 break;
241
243 break;
244 }
245
246 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_category"_s, categories[pair.first], true ) );
247 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_value"_s, pair.second, true ) );
248 QgsTextRenderer::drawText( QPointF( labelX, labelY + labelYAdjustment ), 0, horizontalAlignment, { text }, context, mLabelTextFormat );
249
250 ySum += pair.second;
251 }
252 }
253 }
254
255 symbol->stopRender( context );
256 symbol->setColor( symbolColor );
257 seriesIndex++;
258 }
259}
260
262{
263 if ( index < 0 || index >= static_cast<int>( mFillSymbols.size() ) )
264 {
265 return nullptr;
266 }
267
268 return mFillSymbols[index].get();
269}
270
272{
273 if ( index < 0 )
274 {
275 return;
276 }
277
278 if ( index + 1 >= static_cast<int>( mFillSymbols.size() ) )
279 {
280 mFillSymbols.resize( index + 1 );
281 }
282
283 mFillSymbols[index].reset( symbol );
284}
285
287{
288 if ( index < 0 || index >= static_cast<int>( mColorRamps.size() ) )
289 {
290 return nullptr;
291 }
292
293 return mColorRamps[index].get();
294}
295
297{
298 if ( index < 0 )
299 {
300 return;
301 }
302
303 if ( index + 1 >= static_cast<int>( mColorRamps.size() ) )
304 {
305 mColorRamps.resize( index + 1 );
306 }
307
308 mColorRamps[index].reset( ramp );
309}
310
311bool QgsPieChartPlot::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const
312{
313 Qgs2DPlot::writeXml( element, document, context );
314
315 QDomElement fillSymbolsElement = document.createElement( u"fillSymbols"_s );
316 for ( int i = 0; i < static_cast<int>( mFillSymbols.size() ); i++ )
317 {
318 QDomElement fillSymbolElement = document.createElement( u"fillSymbol"_s );
319 fillSymbolElement.setAttribute( u"index"_s, QString::number( i ) );
320 if ( mFillSymbols[i] )
321 {
322 fillSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mFillSymbols[i].get(), document, context ) );
323 }
324 fillSymbolsElement.appendChild( fillSymbolElement );
325 }
326 element.appendChild( fillSymbolsElement );
327
328 QDomElement colorRampsElement = document.createElement( u"colorRamps"_s );
329 for ( int i = 0; i < static_cast<int>( mColorRamps.size() ); i++ )
330 {
331 QDomElement colorRampElement = document.createElement( u"colorRamp"_s );
332 colorRampElement.setAttribute( u"index"_s, QString::number( i ) );
333 if ( mColorRamps[i] )
334 {
335 colorRampElement.appendChild( QgsSymbolLayerUtils::saveColorRamp( QString(), mColorRamps[i].get(), document ) );
336 }
337 colorRampsElement.appendChild( colorRampElement );
338 }
339 element.appendChild( colorRampsElement );
340
341 QDomElement textFormatElement = document.createElement( u"textFormat"_s );
342 textFormatElement.appendChild( mLabelTextFormat.writeXml( document, context ) );
343 element.appendChild( textFormatElement );
344
345 if ( mNumericFormat )
346 {
347 QDomElement numericFormatElement = document.createElement( u"numericFormat"_s );
348 mNumericFormat->writeXml( numericFormatElement, document, context );
349 element.appendChild( numericFormatElement );
350 }
351
352 element.setAttribute( u"pieChartLabelType"_s, qgsEnumValueToKey( mLabelType ) );
353
354 return true;
355}
356
357bool QgsPieChartPlot::readXml( const QDomElement &element, const QgsReadWriteContext &context )
358{
359 Qgs2DPlot::readXml( element, context );
360
361 const QDomNodeList fillSymbolsList = element.firstChildElement( u"fillSymbols"_s ).childNodes();
362 for ( int i = 0; i < fillSymbolsList.count(); i++ )
363 {
364 const QDomElement fillSymbolElement = fillSymbolsList.at( i ).toElement();
365 const int index = fillSymbolElement.attribute( u"index"_s, u"-1"_s ).toInt();
366 if ( index >= 0 )
367 {
368 if ( fillSymbolElement.hasChildNodes() )
369 {
370 const QDomElement symbolElement = fillSymbolElement.firstChildElement( u"symbol"_s );
371 setFillSymbolAt( index, QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElement, context ).release() );
372 }
373 else
374 {
375 setFillSymbolAt( index, nullptr );
376 }
377 }
378 }
379
380 const QDomNodeList colorRampsList = element.firstChildElement( u"colorRamps"_s ).childNodes();
381 for ( int i = 0; i < colorRampsList.count(); i++ )
382 {
383 const QDomElement colorRampElement = colorRampsList.at( i ).toElement();
384 const int index = colorRampElement.attribute( u"index"_s, u"-1"_s ).toInt();
385 if ( index >= 0 )
386 {
387 if ( colorRampElement.hasChildNodes() )
388 {
389 QDomElement rampElement = colorRampElement.firstChildElement( u"colorramp"_s );
390 setColorRampAt( index, QgsSymbolLayerUtils::loadColorRamp( rampElement ).release() );
391 }
392 else
393 {
394 setColorRampAt( index, nullptr );
395 }
396 }
397 }
398
399 const QDomElement textFormatElement = element.firstChildElement( u"textFormat"_s );
400 mLabelTextFormat.readXml( textFormatElement, context );
401
402 const QDomElement numericFormatElement = element.firstChildElement( u"numericFormat"_s );
403 if ( !numericFormatElement.isNull() )
404 {
405 mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElement, context ) );
406 }
407 else
408 {
409 mNumericFormat.reset();
410 }
411
412 mLabelType = qgsEnumKeyToValue( element.attribute( u"pieChartLabelType"_s ), Qgis::PieChartLabelType::NoLabels );
413
414 return true;
415}
416
421
423{
424 QgsPieChartPlot *chart = dynamic_cast<QgsPieChartPlot *>( plot );
425 if ( !chart )
426 {
427 return nullptr;
428 }
429
431}
432
434{
435 mLabelTextFormat = format;
436}
437
439{
440 mNumericFormat.reset( format );
441}
442
447
449{
450 if ( !plot )
451 {
452 return;
453 }
454
455 // pie charts do not have axis, so we transfer only settings related to the chart area
456 if ( const Qgs2DPlot *plot2D = dynamic_cast<const Qgs2DPlot *>( plot ) )
457 {
459 }
460
461 if ( const QgsPieChartPlot *pieChartPlot = dynamic_cast<const QgsPieChartPlot *>( plot ) )
462 {
463 for ( int idx = 0; idx < pieChartPlot->fillSymbolCount(); idx++ )
464 {
465 setFillSymbolAt( idx, pieChartPlot->fillSymbolAt( idx )->clone() );
466 }
467 }
468}
PieChartLabelType
Pie chart label types.
Definition qgis.h:3584
@ Categories
Category labels are drawn.
Definition qgis.h:3586
@ Values
Value labels are drawn.
Definition qgis.h:3587
@ NoLabels
Labels are not drawn.
Definition qgis.h:3585
@ Categorical
The axis represents categories.
Definition qgis.h:3574
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3167
@ Center
Center align.
Definition qgis.h:3169
bool writeXml(QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context) const override
Writes the plot's properties into an XML element.
Definition qgsplot.cpp:281
void copyCommonProperties(const Qgs2DPlot *other)
Copies all Qgs2DPlot-level properties (size, margins, data-defined properties) from other to this plo...
Definition qgsplot.cpp:397
Qgs2DPlot()
Constructor for Qgs2DPlot.
Definition qgsplot.cpp:277
bool readXml(const QDomElement &element, const QgsReadWriteContext &context) override
Reads the plot's properties from an XML element.
Definition qgsplot.cpp:290
An abstract class used to encapsulate the data for a plot series.
Definition qgsplot.h:210
static QgsNumericFormatRegistry * numericFormatRegistry()
Gets the registry of available numeric formats.
Abstract base class for color ramps.
virtual QColor color(double value) const =0
Returns the color corresponding to a specified value.
RAII class to pop scope from an expression context on destruction.
Single scope for storing variables and functions for use within a QgsExpressionContext.
void addVariable(const QgsExpressionContextScope::StaticVariable &variable)
Adds a variable into the context scope.
A fill symbol type, for rendering Polygon and MultiPolygon geometries.
void renderPolygon(const QPolygonF &points, const QVector< QPolygonF > *rings, const QgsFeature *f, QgsRenderContext &context, int layer=-1, bool selected=false)
Renders the symbol using the given render context.
A context for numeric formats.
Abstract base class for numeric formatters, which allow for formatting a numeric value for display.
void setTextFormat(const QgsTextFormat &format)
Sets the text format used for the pie chart labels.
QgsFillSymbol * fillSymbolAt(int index) const
Returns the fill symbol for the series with matching index.
void setNumericFormat(QgsNumericFormat *format)
Sets the numeric format used for the pie chart labels.
QgsColorRamp * colorRampAt(int index) const
Returns the color ramp for the series with matching index.
void setLabelType(Qgis::PieChartLabelType type)
Sets the pie chart label type.
bool readXml(const QDomElement &element, const QgsReadWriteContext &context) override
Reads the plot's properties from an XML element.
bool writeXml(QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context) const override
Writes the plot's properties into an XML element.
static QgsPieChartPlot * create()
Returns a new pie chart.
void initFromPlot(const QgsPlot *plot) override
Initializes properties of this plot from an existing plot, transferring all applicable settings.
void setColorRampAt(int index, QgsColorRamp *ramp)
Sets the color ramp for the series with matching index.
void setFillSymbolAt(int index, QgsFillSymbol *symbol)
Sets the fill symbol to use for the series with matching index.
QString type() const override
Returns the plot's type.
void renderContent(QgsRenderContext &context, QgsPlotRenderContext &plotContext, const QRectF &plotArea, const QgsPlotData &plotData=QgsPlotData()) override
Renders the plot content.
static QgsVectorLayerAbstractPlotDataGatherer * createDataGatherer(QgsPlot *plot)
Returns a new data gatherer for a given pie chart plot.
Encapsulates one or more plot series.
Definition qgsplot.h:303
QStringList categories() const
Returns the name of the series' categories.
Definition qgsplot.cpp:1518
QList< QgsAbstractPlotSeries * > series() const
Returns the list of series forming the plot data.
Definition qgsplot.cpp:1499
static QgsNumericFormat * pieChartNumericFormat()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1444
static QgsColorRamp * pieChartColorRamp()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1437
static QgsFillSymbol * pieChartFillSymbol()
Returns the default fill symbol to use for pie charts.
Definition qgsplot.cpp:1431
Contains information about the context of a plot rendering operation.
Definition qgsplot.h:191
QgsPlot()=default
A color ramp consisting of random colors, constrained within component ranges.
A container for the context for various read/write operations on objects.
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
Scoped object for saving and restoring a QPainter object's state.
static std::unique_ptr< QgsColorRamp > loadColorRamp(QDomElement &element)
Creates a color ramp from the settings encoded in an XML element.
static std::unique_ptr< QgsSymbol > loadSymbol(const QDomElement &element, const QgsReadWriteContext &context)
Attempts to load a symbol from a DOM element.
static QDomElement saveSymbol(const QString &symbolName, const QgsSymbol *symbol, QDomDocument &doc, const QgsReadWriteContext &context)
Writes a symbol definition to XML.
static QDomElement saveColorRamp(const QString &name, const QgsColorRamp *ramp, QDomDocument &doc)
Encodes a color ramp's settings to an XML element.
static double estimateMaxSymbolBleed(QgsSymbol *symbol, const QgsRenderContext &context)
Returns the maximum estimated bleed for the symbol.
void stopRender(QgsRenderContext &context)
Ends the rendering process.
void setColor(const QColor &color) const
Sets the color for the symbol.
QColor color() const
Returns the symbol's color.
void startRender(QgsRenderContext &context, const QgsFields &fields=QgsFields())
Begins the rendering process for the symbol.
Container for all settings relating to text rendering.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
An abstract vector layer plot data gatherer base class.
An vector layer plot data gatherer class for XY series.
Encapsulates the data for an XY plot series.
Definition qgsplot.h:263
T qgsEnumKeyToValue(const QString &key, const T &defaultValue, bool tryValueAsKey=true, bool *returnOk=nullptr)
Returns the value corresponding to the given key of an enum.
Definition qgis.h:8018
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:7999
Single variable definition for use within a QgsExpressionContextScope.