QGIS API Documentation 3.99.0-Master (c22de0620c0)
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 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_series_name"_s, series->name(), true ) );
139
140 if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
141 {
142 const QList<std::pair<double, double>> data = xySeries->data();
143 double yTotal = 0;
144 for ( const std::pair<double, double> &pair : data )
145 {
146 if ( !categoriesColor.contains( categories[pair.first] ) )
147 {
148 if ( ramp )
149 {
150 categoriesColor[categories[pair.first]] = ramp->color( pair.first / ( categories.size() - 1 ) );
151 }
152 else
153 {
154 categoriesColor[categories[pair.first]] = symbolColor;
155 }
156 }
157
158 yTotal += pair.second;
159 }
160
161 double ySum = 0;
162 for ( const std::pair<double, double> &pair : data )
163 {
164 QPointF center;
165 if ( pieStackHorizontal )
166 {
167 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
168 }
169 else
170 {
171 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
172 }
173 QRectF boundingBox( center.x() - pieWidth / 2, center.y() - pieWidth / 2, pieWidth, pieWidth );
174
175 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
176 const double degreesForward = pair.second / yTotal * 360;
177
178 QPainterPath path;
179 path.moveTo( center );
180 path.arcTo( boundingBox, -degreesStart, -degreesForward );
181
182 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_category"_s, categories[pair.first], true ) );
183 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_value"_s, pair.second, true ) );
184 symbol->setColor( categoriesColor[categories[pair.first]] );
185 symbol->renderPolygon( path.toFillPolygon(), nullptr, nullptr, context );
186
187 ySum += pair.second;
188 }
189
190 if ( mLabelType != Qgis::PieChartLabelType::NoLabels )
191 {
192 QString text;
193 ySum = 0;
194 for ( const std::pair<double, double> &pair : data )
195 {
196 QPointF center;
197 if ( pieStackHorizontal )
198 {
199 center = QPointF( plotArea.x() + ( ( plotArea.width() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ), plotArea.y() + plotArea.height() / 2 );
200 }
201 else
202 {
203 center = QPointF( plotArea.x() + plotArea.width() / 2, plotArea.y() + ( ( plotArea.height() - pieArea * pieStackCount ) / 2 + pieArea * seriesIndex + pieArea / 2 ) );
204 }
205
206 const double degreesStart = ( ySum / yTotal * 360 ) - 90; // adjust angle so we start on top
207 const double degreesForward = pair.second / yTotal * 360;
208 const double degreesMid = ( degreesStart + ( degreesForward / 2 ) );
209
210 const double labelX = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::cos( degreesMid * M_PI / 180 ) + center.x();
211 const double labelY = ( ( pieWidth + maxLabelHeight ) / 2 ) * std::sin( degreesMid * M_PI / 180 ) + center.y();
212 const double labelYAdjustment = degreesMid > 0 && degreesMid <= 180 ? maxLabelHeight / 2 : 0;
213
215 if ( degreesMid < -85 || ( degreesMid > 85 && degreesMid <= 95 ) || degreesMid > 265 )
216 {
217 horizontalAlignment = Qgis::TextHorizontalAlignment::Center;
218 }
219 else if ( degreesMid > 95 && degreesMid <= 265 )
220 {
221 horizontalAlignment = Qgis::TextHorizontalAlignment::Right;
222 }
223
224 switch ( mLabelType )
225 {
227 text = categories[pair.first];
228 break;
229
231 if ( mNumericFormat )
232 {
233 text = mNumericFormat->formatDouble( pair.second, numericContext );
234 }
235 else
236 {
237 text = QString::number( pair.second );
238 }
239 break;
240
242 break;
243 }
244
245 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_category"_s, categories[pair.first], true ) );
246 chartScope->addVariable( QgsExpressionContextScope::StaticVariable( u"chart_value"_s, pair.second, true ) );
247 QgsTextRenderer::drawText( QPointF( labelX, labelY + labelYAdjustment ), 0, horizontalAlignment, { text }, context, mLabelTextFormat );
248
249 ySum += pair.second;
250 }
251 }
252 }
253
254 symbol->stopRender( context );
255 symbol->setColor( symbolColor );
256 seriesIndex++;
257 }
258
259 context.painter()->restore();
260}
261
263{
264 if ( index < 0 || index >= static_cast<int>( mFillSymbols.size() ) )
265 {
266 return nullptr;
267 }
268
269 return mFillSymbols[index].get();
270}
271
273{
274 if ( index < 0 )
275 {
276 return;
277 }
278
279 if ( index + 1 >= static_cast<int>( mFillSymbols.size() ) )
280 {
281 mFillSymbols.resize( index + 1 );
282 }
283
284 mFillSymbols[index].reset( symbol );
285}
286
288{
289 if ( index < 0 || index >= static_cast<int>( mColorRamps.size() ) )
290 {
291 return nullptr;
292 }
293
294 return mColorRamps[index].get();
295}
296
298{
299 if ( index < 0 )
300 {
301 return;
302 }
303
304 if ( index + 1 >= static_cast<int>( mColorRamps.size() ) )
305 {
306 mColorRamps.resize( index + 1 );
307 }
308
309 mColorRamps[index].reset( ramp );
310}
311
312bool QgsPieChartPlot::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const
313{
314 Qgs2DPlot::writeXml( element, document, context );
315
316 QDomElement fillSymbolsElement = document.createElement( u"fillSymbols"_s );
317 for ( int i = 0; i < static_cast<int>( mFillSymbols.size() ); i++ )
318 {
319 QDomElement fillSymbolElement = document.createElement( u"fillSymbol"_s );
320 fillSymbolElement.setAttribute( u"index"_s, QString::number( i ) );
321 if ( mFillSymbols[i] )
322 {
323 fillSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mFillSymbols[i].get(), document, context ) );
324 }
325 fillSymbolsElement.appendChild( fillSymbolElement );
326 }
327 element.appendChild( fillSymbolsElement );
328
329 QDomElement colorRampsElement = document.createElement( u"colorRamps"_s );
330 for ( int i = 0; i < static_cast<int>( mColorRamps.size() ); i++ )
331 {
332 QDomElement colorRampElement = document.createElement( u"colorRamp"_s );
333 colorRampElement.setAttribute( u"index"_s, QString::number( i ) );
334 if ( mColorRamps[i] )
335 {
336 colorRampElement.appendChild( QgsSymbolLayerUtils::saveColorRamp( QString(), mColorRamps[i].get(), document ) );
337 }
338 colorRampsElement.appendChild( colorRampElement );
339 }
340 element.appendChild( colorRampsElement );
341
342 QDomElement textFormatElement = document.createElement( u"textFormat"_s );
343 textFormatElement.appendChild( mLabelTextFormat.writeXml( document, context ) );
344 element.appendChild( textFormatElement );
345
346 if ( mNumericFormat )
347 {
348 QDomElement numericFormatElement = document.createElement( u"numericFormat"_s );
349 mNumericFormat->writeXml( numericFormatElement, document, context );
350 element.appendChild( numericFormatElement );
351 }
352
353 element.setAttribute( u"pieChartLabelType"_s, qgsEnumValueToKey( mLabelType ) );
354
355 return true;
356}
357
358bool QgsPieChartPlot::readXml( const QDomElement &element, const QgsReadWriteContext &context )
359{
360 Qgs2DPlot::readXml( element, context );
361
362 const QDomNodeList fillSymbolsList = element.firstChildElement( u"fillSymbols"_s ).childNodes();
363 for ( int i = 0; i < fillSymbolsList.count(); i++ )
364 {
365 const QDomElement fillSymbolElement = fillSymbolsList.at( i ).toElement();
366 const int index = fillSymbolElement.attribute( u"index"_s, u"-1"_s ).toInt();
367 if ( index >= 0 )
368 {
369 if ( fillSymbolElement.hasChildNodes() )
370 {
371 const QDomElement symbolElement = fillSymbolElement.firstChildElement( u"symbol"_s );
372 setFillSymbolAt( index, QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElement, context ).release() );
373 }
374 else
375 {
376 setFillSymbolAt( index, nullptr );
377 }
378 }
379 }
380
381 const QDomNodeList colorRampsList = element.firstChildElement( u"colorRamps"_s ).childNodes();
382 for ( int i = 0; i < colorRampsList.count(); i++ )
383 {
384 const QDomElement colorRampElement = colorRampsList.at( i ).toElement();
385 const int index = colorRampElement.attribute( u"index"_s, u"-1"_s ).toInt();
386 if ( index >= 0 )
387 {
388 if ( colorRampElement.hasChildNodes() )
389 {
390 QDomElement rampElement = colorRampElement.firstChildElement( u"colorramp"_s );
391 setColorRampAt( index, QgsSymbolLayerUtils::loadColorRamp( rampElement ).release() );
392 }
393 else
394 {
395 setColorRampAt( index, nullptr );
396 }
397 }
398 }
399
400 const QDomElement textFormatElement = element.firstChildElement( u"textFormat"_s );
401 mLabelTextFormat.readXml( textFormatElement, context );
402
403 const QDomElement numericFormatElement = element.firstChildElement( u"numericFormat"_s );
404 if ( !numericFormatElement.isNull() )
405 {
406 mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElement, context ) );
407 }
408 else
409 {
410 mNumericFormat.reset();
411 }
412
413 mLabelType = qgsEnumKeyToValue( element.attribute( u"pieChartLabelType"_s ), Qgis::PieChartLabelType::NoLabels );
414
415 return true;
416}
417
422
424{
425 QgsPieChartPlot *chart = dynamic_cast<QgsPieChartPlot *>( plot );
426 if ( !chart )
427 {
428 return nullptr;
429 }
430
432}
433
435{
436 mLabelTextFormat = format;
437}
438
440{
441 mNumericFormat.reset( format );
442}
443
PieChartLabelType
Pie chart label types.
Definition qgis.h:3432
@ Categories
Category labels are drawn.
Definition qgis.h:3434
@ Values
Value labels are drawn.
Definition qgis.h:3435
@ NoLabels
Labels are not drawn.
Definition qgis.h:3433
@ Categorical
The axis represents categories.
Definition qgis.h:3422
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3015
@ Center
Center align.
Definition qgis.h:3017
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:1381
QList< QgsAbstractPlotSeries * > series() const
Returns the list of series forming the plot data.
Definition qgsplot.cpp:1362
static QgsNumericFormat * pieChartNumericFormat()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1306
static QgsColorRamp * pieChartColorRamp()
Returns the default color ramp to use for pie charts.
Definition qgsplot.cpp:1301
static QgsFillSymbol * pieChartFillSymbol()
Returns the default fill symbol to use for pie charts.
Definition qgsplot.cpp:1295
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:7160
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:7141
Single variable definition for use within a QgsExpressionContextScope.