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