QGIS API Documentation 3.99.0-Master (21b3aa880ba)
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 "qgssymbol.h"
25#include "qgssymbollayer.h"
26#include "qgssymbollayerutils.h"
27#include "qgstextrenderer.h"
29
36
37void QgsPieChartPlot::renderContent( QgsRenderContext &context, QgsPlotRenderContext &, const QRectF &plotArea, const QgsPlotData &plotData )
38{
39 if ( mFillSymbols.empty() )
40 {
41 return;
42 }
43
44 const QList<QgsAbstractPlotSeries *> seriesList = plotData.series();
45 if ( seriesList.isEmpty() || plotData.categories().isEmpty() )
46 {
47 return;
48 }
49
50 const QStringList categories = plotData.categories();
51 double maxLabelHeight = 0;
52 switch ( mLabelType )
53 {
55 {
56 for ( const QString &category : categories )
57 {
58 maxLabelHeight = std::max( maxLabelHeight, QgsTextRenderer::textHeight( context, mLabelTextFormat, { category } ) );
59 }
60 break;
61 }
62
64 {
65 QgsNumericFormatContext numericContext;
66 QString text;
67
68 for ( const QgsAbstractPlotSeries *series : seriesList )
69 {
70 if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
71 {
72 const QList<std::pair<double, double>> data = xySeries->data();
73 for ( const std::pair<double, double> &pair : data )
74 {
75 if ( mNumericFormat )
76 {
77 text = mNumericFormat->formatDouble( pair.second, numericContext );
78 }
79 else
80 {
81 text = QString::number( pair.second );
82 }
83 maxLabelHeight = std::max( maxLabelHeight, QgsTextRenderer::textHeight( context, mLabelTextFormat, { text } ) );
84 }
85 }
86 }
87 break;
88 }
89
91 break;
92 }
93
94 QgsExpressionContextScope *chartScope = new QgsExpressionContextScope( QStringLiteral( "chart" ) );
95 const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), chartScope );
96
97 context.painter()->save();
98 context.painter()->setClipRect( plotArea );
99
100 const bool pieStackHorizontal = plotArea.width() >= plotArea.height();
101 const double pieStackCount = seriesList.size();
102 double pieArea = 0;
103 if ( pieStackHorizontal )
104 {
105 pieArea = plotArea.height() * pieStackCount > plotArea.width() ? plotArea.width() / pieStackCount : plotArea.height();
106 }
107 else
108 {
109 pieArea = plotArea.width() * pieStackCount > plotArea.height() ? plotArea.height() / pieStackCount : plotArea.width();
110 }
111
112 QgsNumericFormatContext numericContext;
113 QMap<QString, QColor> categoriesColor;
114 int seriesIndex = 0;
115 for ( const QgsAbstractPlotSeries *series : seriesList )
116 {
117 QgsFillSymbol *symbol = fillSymbolAt( seriesIndex % mFillSymbols.size() );
118 if ( !symbol )
119 {
120 continue;
121 }
122 const QColor symbolColor = symbol->color();
123 symbol->startRender( context );
124 const double pieWidth = pieArea - QgsSymbolLayerUtils::estimateMaxSymbolBleed( symbol, context ) - maxLabelHeight * 3;
125
126 QgsColorRamp *ramp = colorRampAt( seriesIndex % mColorRamps.size() );
127 if ( QgsRandomColorRamp *randomRamp = dynamic_cast<QgsRandomColorRamp *>( ramp ) )
128 {
129 //ramp is a random colors ramp, so inform it of the total number of required colors
130 //this allows the ramp to pregenerate a set of visually distinctive colors
131 randomRamp->setTotalColorCount( categories.size() );
132 }
133
134 if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
135 {
136 const QList<std::pair<double, double>> data = xySeries->data();
137 double yTotal = 0;
138 for ( const std::pair<double, double> &pair : data )
139 {
140 if ( !categoriesColor.contains( categories[pair.first] ) )
141 {
142 if ( ramp )
143 {
144 categoriesColor[categories[pair.first]] = ramp->color( pair.first / ( categories.size() - 1 ) );
145 }
146 else
147 {
148 categoriesColor[categories[pair.first]] = symbolColor;
149 }
150 }
151
152 yTotal += pair.second;
153 }
154
155 double ySum = 0;
156 for ( const std::pair<double, double> &pair : data )
157 {
158 QPointF center;
159 if ( pieStackHorizontal )
160 {
161 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
162 }
163 else
164 {
165 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
166 }
167 QRectF boundingBox( center.x() - pieWidth / 2, center.y() - pieWidth / 2, pieWidth, pieWidth );
168
169 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
170 const double degreesForward = pair.second / yTotal * 360;
171
172 QPainterPath path;
173 path.moveTo( center );
174 path.arcTo( boundingBox, -degreesStart, -degreesForward );
175
176 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_category" ), categories[pair.first], true ) );
177 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_value" ), pair.second, true ) );
178 symbol->setColor( categoriesColor[categories[pair.first]] );
179 symbol->renderPolygon( path.toFillPolygon(), nullptr, nullptr, context );
180
181 ySum += pair.second;
182 }
183
184 if ( mLabelType != Qgis::PieChartLabelType::NoLabels )
185 {
186 QString text;
187 ySum = 0;
188 for ( const std::pair<double, double> &pair : data )
189 {
190 QPointF center;
191 if ( pieStackHorizontal )
192 {
193 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
194 }
195 else
196 {
197 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
198 }
199
200 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
201 const double degreesForward = pair.second / yTotal * 360;
202 const double degreesMid = ( degreesStart + ( degreesForward / 2 ) );
203
204 const double labelX = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::cos( degreesMid * M_PI / 180 ) + center.x();
205 const double labelY = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::sin( degreesMid * M_PI / 180 ) + center.y();
206 const double labelYAdjustment = degreesMid > 0 && degreesMid <= 180 ? maxLabelHeight / 2 : 0;
207
209 if ( degreesMid < -85 || ( degreesMid > 85 && degreesMid <= 95 ) || degreesMid > 265 )
210 {
211 horizontalAlignment = Qgis::TextHorizontalAlignment::Center;
212 }
213 else if ( degreesMid > 95 && degreesMid <= 265 )
214 {
215 horizontalAlignment = Qgis::TextHorizontalAlignment::Right;
216 }
217
218 switch ( mLabelType )
219 {
221 text = categories[pair.first];
222 break;
223
225 if ( mNumericFormat )
226 {
227 text = mNumericFormat->formatDouble( pair.second, numericContext );
228 }
229 else
230 {
231 text = QString::number( pair.second );
232 }
233 break;
234
236 break;
237 }
238
239 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_category" ), categories[pair.first], true ) );
240 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_value" ), pair.second, true ) );
241 QgsTextRenderer::drawText( QPointF( labelX, labelY + labelYAdjustment ), 0, horizontalAlignment, { text }, context, mLabelTextFormat );
242
243 ySum += pair.second;
244 }
245 }
246 }
247
248 symbol->stopRender( context );
249 symbol->setColor( symbolColor );
250 seriesIndex++;
251 }
252
253 context.painter()->restore();
254}
255
257{
258 if ( index < 0 || index >= static_cast<int>( mFillSymbols.size() ) )
259 {
260 return nullptr;
261 }
262
263 return mFillSymbols[index].get();
264}
265
267{
268 if ( index < 0 )
269 {
270 return;
271 }
272
273 if ( index + 1 >= static_cast<int>( mFillSymbols.size() ) )
274 {
275 mFillSymbols.resize( index + 1 );
276 }
277
278 mFillSymbols[index].reset( symbol );
279}
280
282{
283 if ( index < 0 || index >= static_cast<int>( mColorRamps.size() ) )
284 {
285 return nullptr;
286 }
287
288 return mColorRamps[index].get();
289}
290
292{
293 if ( index < 0 )
294 {
295 return;
296 }
297
298 if ( index + 1 >= static_cast<int>( mColorRamps.size() ) )
299 {
300 mColorRamps.resize( index + 1 );
301 }
302
303 mColorRamps[index].reset( ramp );
304}
305
306bool QgsPieChartPlot::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const
307{
308 Qgs2DPlot::writeXml( element, document, context );
309
310 QDomElement fillSymbolsElement = document.createElement( QStringLiteral( "fillSymbols" ) );
311 for ( int i = 0; i < static_cast<int>( mFillSymbols.size() ); i++ )
312 {
313 QDomElement fillSymbolElement = document.createElement( QStringLiteral( "fillSymbol" ) );
314 fillSymbolElement.setAttribute( QStringLiteral( "index" ), QString::number( i ) );
315 if ( mFillSymbols[i] )
316 {
317 fillSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mFillSymbols[i].get(), document, context ) );
318 }
319 fillSymbolsElement.appendChild( fillSymbolElement );
320 }
321 element.appendChild( fillSymbolsElement );
322
323 QDomElement colorRampsElement = document.createElement( QStringLiteral( "colorRamps" ) );
324 for ( int i = 0; i < static_cast<int>( mColorRamps.size() ); i++ )
325 {
326 QDomElement colorRampElement = document.createElement( QStringLiteral( "colorRamp" ) );
327 colorRampElement.setAttribute( QStringLiteral( "index" ), QString::number( i ) );
328 if ( mColorRamps[i] )
329 {
330 colorRampElement.appendChild( QgsSymbolLayerUtils::saveColorRamp( QString(), mColorRamps[i].get(), document ) );
331 }
332 colorRampsElement.appendChild( colorRampElement );
333 }
334 element.appendChild( colorRampsElement );
335
336 QDomElement textFormatElement = document.createElement( QStringLiteral( "textFormat" ) );
337 textFormatElement.appendChild( mLabelTextFormat.writeXml( document, context ) );
338 element.appendChild( textFormatElement );
339
340 if ( mNumericFormat )
341 {
342 QDomElement numericFormatElement = document.createElement( QStringLiteral( "numericFormat" ) );
343 mNumericFormat->writeXml( numericFormatElement, document, context );
344 element.appendChild( numericFormatElement );
345 }
346
347 element.setAttribute( QStringLiteral( "pieChartLabelType" ), qgsEnumValueToKey( mLabelType ) );
348
349 return true;
350}
351
352bool QgsPieChartPlot::readXml( const QDomElement &element, const QgsReadWriteContext &context )
353{
354 Qgs2DPlot::readXml( element, context );
355
356 const QDomNodeList fillSymbolsList = element.firstChildElement( QStringLiteral( "fillSymbols" ) ).childNodes();
357 for ( int i = 0; i < fillSymbolsList.count(); i++ )
358 {
359 const QDomElement fillSymbolElement = fillSymbolsList.at( i ).toElement();
360 const int index = fillSymbolElement.attribute( QStringLiteral( "index" ), QStringLiteral( "-1" ) ).toInt();
361 if ( index >= 0 )
362 {
363 if ( fillSymbolElement.hasChildNodes() )
364 {
365 const QDomElement symbolElement = fillSymbolElement.firstChildElement( QStringLiteral( "symbol" ) );
366 setFillSymbolAt( index, QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElement, context ).release() );
367 }
368 else
369 {
370 setFillSymbolAt( index, nullptr );
371 }
372 }
373 }
374
375 const QDomNodeList colorRampsList = element.firstChildElement( QStringLiteral( "colorRamps" ) ).childNodes();
376 for ( int i = 0; i < colorRampsList.count(); i++ )
377 {
378 const QDomElement colorRampElement = colorRampsList.at( i ).toElement();
379 const int index = colorRampElement.attribute( QStringLiteral( "index" ), QStringLiteral( "-1" ) ).toInt();
380 if ( index >= 0 )
381 {
382 if ( colorRampElement.hasChildNodes() )
383 {
384 QDomElement rampElement = colorRampElement.firstChildElement( QStringLiteral( "colorramp" ) );
385 setColorRampAt( index, QgsSymbolLayerUtils::loadColorRamp( rampElement ).release() );
386 }
387 else
388 {
389 setColorRampAt( index, nullptr );
390 }
391 }
392 }
393
394 const QDomElement textFormatElement = element.firstChildElement( QStringLiteral( "textFormat" ) );
395 mLabelTextFormat.readXml( textFormatElement, context );
396
397 const QDomElement numericFormatElement = element.firstChildElement( QStringLiteral( "numericFormat" ) );
398 if ( !numericFormatElement.isNull() )
399 {
400 mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElement, context ) );
401 }
402 else
403 {
404 mNumericFormat.reset();
405 }
406
407 mLabelType = qgsEnumKeyToValue( element.attribute( QStringLiteral( "pieChartLabelType" ) ), Qgis::PieChartLabelType::NoLabels );
408
409 return true;
410}
411
416
418{
419 QgsPieChartPlot *chart = dynamic_cast<QgsPieChartPlot *>( plot );
420 if ( !chart )
421 {
422 return nullptr;
423 }
424
426}
427
429{
430 mLabelTextFormat = format;
431}
432
434{
435 mNumericFormat.reset( format );
436}
437
PieChartLabelType
Pie chart label types.
Definition qgis.h:3346
@ Categories
Category labels are drawn.
Definition qgis.h:3348
@ Values
Value labels are drawn.
Definition qgis.h:3349
@ NoLabels
Labels are not drawn.
Definition qgis.h:3347
@ Categorical
The axis represents categories.
Definition qgis.h:3336
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:2942
@ Center
Center align.
Definition qgis.h:2944
bool writeXml(QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context) const override
Writes the plot's properties into an XML element.
Definition qgsplot.cpp:234
bool readXml(const QDomElement &element, const QgsReadWriteContext &context) override
Reads the plot's properties from an XML element.
Definition qgsplot.cpp:243
An abstract class used to encapsulate the data for a plot series.
Definition qgsplot.h:204
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 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:300
QStringList categories() const
Returns the name of the series' categories.
Definition qgsplot.cpp:1300
QList< QgsAbstractPlotSeries * > series() const
Returns the list of series forming the plot data.
Definition qgsplot.cpp:1281
static QgsNumericFormat * pieChartNumericFormat()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1225
static QgsColorRamp * pieChartColorRamp()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1220
static QgsFillSymbol * pieChartFillSymbol()
Returns the default fill symbol to use for pie charts.
Definition qgsplot.cpp:1214
Contains information about the context of a plot rendering operation.
Definition qgsplot.h:184
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.
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:258
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:6817
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:6798
Single variable definition for use within a QgsExpressionContextScope.