1 /***************************************************************************
2 qgshistogramdiagram.cpp
3 ---------------------
4 begin : August 2012
5 copyright : (C) 2012 by Matthias Kuhn
6 email : matthias at opengis dot ch
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15 #include "qgshistogramdiagram.h"
16 #include "qgsdiagramrenderer.h"
17 #include "qgsrendercontext.h"
18 #include "qgsexpression.h"
19 #include "qgssymbollayerutils.h"
20 #include "qgslinesymbol.h"
21
22 #include <QPainter>
23
QgsHistogramDiagram()24 QgsHistogramDiagram::QgsHistogramDiagram()
25 {
26 mCategoryBrush.setStyle( Qt::SolidPattern );
27 mPen.setStyle( Qt::SolidLine );
28 mScaleFactor = 0;
29 }
30
clone() const31 QgsHistogramDiagram *QgsHistogramDiagram::clone() const
32 {
33 return new QgsHistogramDiagram( *this );
34 }
35
diagramSize(const QgsFeature & feature,const QgsRenderContext & c,const QgsDiagramSettings & s,const QgsDiagramInterpolationSettings & is)36 QSizeF QgsHistogramDiagram::diagramSize( const QgsFeature &feature, const QgsRenderContext &c, const QgsDiagramSettings &s, const QgsDiagramInterpolationSettings &is )
37 {
38 QSizeF size;
39 if ( feature.attributes().isEmpty() )
40 {
41 return size; //zero size if no attributes
42 }
43
44 if ( qgsDoubleNear( is.upperValue, is.lowerValue ) )
45 return size; // invalid value range => zero size
46
47 double maxValue = 0;
48
49 QgsExpressionContext expressionContext = c.expressionContext();
50 expressionContext.setFeature( feature );
51 if ( !feature.fields().isEmpty() )
52 expressionContext.setFields( feature.fields() );
53
54 for ( const QString &cat : std::as_const( s.categoryAttributes ) )
55 {
56 QgsExpression *expression = getExpression( cat, expressionContext );
57 maxValue = std::max( expression->evaluate( &expressionContext ).toDouble(), maxValue );
58 }
59
60 // Scale, if extension is smaller than the specified minimum
61 if ( maxValue < s.minimumSize )
62 {
63 maxValue = s.minimumSize;
64 }
65
66 // eh - this method returns size in unknown units ...! We'll have to fake it and use a rough estimation of
67 // a conversion factor to painter units...
68 // TODO QGIS 4.0 -- these methods should all use painter units, dependent on the render context scaling...
69 double painterUnitConversionScale = c.convertToPainterUnits( 1, s.sizeType );
70
71 const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() ) / painterUnitConversionScale;
72
73 switch ( s.diagramOrientation )
74 {
75 case QgsDiagramSettings::Up:
76 case QgsDiagramSettings::Down:
77 mScaleFactor = ( ( is.upperSize.width() - is.lowerSize.height() ) / ( is.upperValue - is.lowerValue ) );
78 size.scale( s.barWidth * s.categoryAttributes.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), maxValue * mScaleFactor, Qt::IgnoreAspectRatio );
79 break;
80
81 case QgsDiagramSettings::Right:
82 case QgsDiagramSettings::Left:
83 mScaleFactor = ( ( is.upperSize.width() - is.lowerSize.width() ) / ( is.upperValue - is.lowerValue ) );
84 size.scale( maxValue * mScaleFactor, s.barWidth * s.categoryAttributes.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), Qt::IgnoreAspectRatio );
85 break;
86 }
87
88 if ( s.showAxis() && s.axisLineSymbol() )
89 {
90 const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c ) / painterUnitConversionScale;
91 size.setWidth( size.width() + 2 * maxBleed );
92 size.setHeight( size.height() + 2 * maxBleed );
93 }
94
95 return size;
96 }
97
legendSize(double value,const QgsDiagramSettings & s,const QgsDiagramInterpolationSettings & is) const98 double QgsHistogramDiagram::legendSize( double value, const QgsDiagramSettings &s, const QgsDiagramInterpolationSettings &is ) const
99 {
100 if ( qgsDoubleNear( is.upperValue, is.lowerValue ) )
101 return s.minimumSize; // invalid value range => zero size
102
103 // Scale, if extension is smaller than the specified minimum
104 if ( value < s.minimumSize )
105 {
106 value = s.minimumSize;
107 }
108
109 double scaleFactor = ( ( is.upperSize.width() - is.lowerSize.width() ) / ( is.upperValue - is.lowerValue ) );
110 return value * scaleFactor;
111 }
112
diagramName() const113 QString QgsHistogramDiagram::diagramName() const
114 {
115 return DIAGRAM_NAME_HISTOGRAM;
116 }
117
diagramSize(const QgsAttributes & attributes,const QgsRenderContext & c,const QgsDiagramSettings & s)118 QSizeF QgsHistogramDiagram::diagramSize( const QgsAttributes &attributes, const QgsRenderContext &c, const QgsDiagramSettings &s )
119 {
120 Q_UNUSED( c )
121 QSizeF size;
122
123 if ( attributes.isEmpty() )
124 {
125 return QSizeF(); //zero size if no attributes
126 }
127
128 double maxValue = attributes.at( 0 ).toDouble();
129
130 for ( int i = 0; i < attributes.count(); ++i )
131 {
132 maxValue = std::max( attributes.at( i ).toDouble(), maxValue );
133 }
134
135 // eh - this method returns size in unknown units ...! We'll have to fake it and use a rough estimation of
136 // a conversion factor to painter units...
137 // TODO QGIS 4.0 -- these methods should all use painter units, dependent on the render context scaling...
138 double painterUnitConversionScale = c.convertToPainterUnits( 1, s.sizeType );
139
140 const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() ) / painterUnitConversionScale;
141
142 switch ( s.diagramOrientation )
143 {
144 case QgsDiagramSettings::Up:
145 case QgsDiagramSettings::Down:
146 mScaleFactor = maxValue / s.size.height();
147 size.scale( s.barWidth * s.categoryColors.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), s.size.height(), Qt::IgnoreAspectRatio );
148 break;
149
150 case QgsDiagramSettings::Right:
151 case QgsDiagramSettings::Left:
152 mScaleFactor = maxValue / s.size.width();
153 size.scale( s.size.width(), s.barWidth * s.categoryColors.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), Qt::IgnoreAspectRatio );
154 break;
155 }
156
157 if ( s.showAxis() && s.axisLineSymbol() )
158 {
159 const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c ) / painterUnitConversionScale;
160 size.setWidth( size.width() + 2 * maxBleed );
161 size.setHeight( size.height() + 2 * maxBleed );
162 }
163
164 return size;
165 }
166
renderDiagram(const QgsFeature & feature,QgsRenderContext & c,const QgsDiagramSettings & s,QPointF position)167 void QgsHistogramDiagram::renderDiagram( const QgsFeature &feature, QgsRenderContext &c, const QgsDiagramSettings &s, QPointF position )
168 {
169 QPainter *p = c.painter();
170 if ( !p )
171 {
172 return;
173 }
174
175 QList<double> values;
176 double maxValue = 0;
177
178 QgsExpressionContext expressionContext = c.expressionContext();
179 expressionContext.setFeature( feature );
180 if ( !feature.fields().isEmpty() )
181 expressionContext.setFields( feature.fields() );
182
183 values.reserve( s.categoryAttributes.size() );
184 for ( const QString &cat : std::as_const( s.categoryAttributes ) )
185 {
186 QgsExpression *expression = getExpression( cat, expressionContext );
187 double currentVal = expression->evaluate( &expressionContext ).toDouble();
188 values.push_back( currentVal );
189 maxValue = std::max( currentVal, maxValue );
190 }
191
192 double scaledMaxVal = sizePainterUnits( maxValue * mScaleFactor, s, c );
193
194 double currentOffset = 0;
195 double scaledWidth = sizePainterUnits( s.barWidth, s, c );
196
197 const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() );
198
199 double baseX = position.x();
200 double baseY = position.y();
201
202 if ( s.showAxis() && s.axisLineSymbol() )
203 {
204 // if showing axis, the diagram position needs shifting from the default base x so that the axis
205 // line stroke sits within the desired label engine rect (otherwise we risk overlaps of the axis line stroke)
206 const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c );
207 baseX += maxBleed;
208 baseY -= maxBleed;
209 }
210
211
212 mPen.setColor( s.penColor );
213 setPenWidth( mPen, s, c );
214 p->setPen( mPen );
215
216 QList<double>::const_iterator valIt = values.constBegin();
217 QList< QColor >::const_iterator colIt = s.categoryColors.constBegin();
218 for ( ; valIt != values.constEnd(); ++valIt, ++colIt )
219 {
220 double length = sizePainterUnits( *valIt * mScaleFactor, s, c );
221
222 mCategoryBrush.setColor( *colIt );
223 p->setBrush( mCategoryBrush );
224
225 switch ( s.diagramOrientation )
226 {
227 case QgsDiagramSettings::Up:
228 p->drawRect( QRectF( baseX + currentOffset, baseY, scaledWidth, length * -1 ) );
229 break;
230
231 case QgsDiagramSettings::Down:
232 p->drawRect( QRectF( baseX + currentOffset, baseY - scaledMaxVal, scaledWidth, length ) );
233 break;
234
235 case QgsDiagramSettings::Right:
236 p->drawRect( QRectF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) + currentOffset, length, scaledWidth ) );
237 break;
238
239 case QgsDiagramSettings::Left:
240 p->drawRect( QRectF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) + currentOffset, 0 - length, scaledWidth ) );
241 break;
242 }
243
244 currentOffset += scaledWidth + spacing;
245 }
246
247 if ( s.showAxis() && s.axisLineSymbol() )
248 {
249 s.axisLineSymbol()->startRender( c );
250 QPolygonF axisPoints;
251 switch ( s.diagramOrientation )
252 {
253 case QgsDiagramSettings::Up:
254 axisPoints << QPointF( baseX, baseY - scaledMaxVal ) << QPointF( baseX, baseY ) << QPointF( baseX + scaledWidth * values.size() + spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ), baseY );
255 break;
256
257 case QgsDiagramSettings::Down:
258 axisPoints << QPointF( baseX, baseY ) << QPointF( baseX, baseY - scaledMaxVal ) << QPointF( baseX + scaledWidth * values.size() + spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ), baseY - scaledMaxVal );
259 break;
260
261 case QgsDiagramSettings::Right:
262 axisPoints << QPointF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
263 << QPointF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
264 << QPointF( baseX, baseY );
265 break;
266
267 case QgsDiagramSettings::Left:
268 axisPoints << QPointF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
269 << QPointF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
270 << QPointF( baseX + scaledMaxVal, baseY );
271 break;
272 }
273
274 s.axisLineSymbol()->renderPolyline( axisPoints, nullptr, c );
275 s.axisLineSymbol()->stopRender( c );
276 }
277 }
278