diff --git a/python/PyQt6/core/auto_additions/qgslayertreemodellegendnode.py b/python/PyQt6/core/auto_additions/qgslayertreemodellegendnode.py index 876670e4ec60..3f60080a2d42 100644 --- a/python/PyQt6/core/auto_additions/qgslayertreemodellegendnode.py +++ b/python/PyQt6/core/auto_additions/qgslayertreemodellegendnode.py @@ -13,7 +13,11 @@ QgsLayerTreeModelLegendNode.LegendNodeRoles.NodeTypeRole = QgsLayerTreeModelLegendNode.CustomRole.NodeType QgsLayerTreeModelLegendNode.NodeTypeRole.is_monkey_patched = True QgsLayerTreeModelLegendNode.NodeTypeRole.__doc__ = "Type of node. Added in 3.16" -QgsLayerTreeModelLegendNode.CustomRole.__doc__ = "Legend node data roles\n\n.. note::\n\n Prior to QGIS 3.36 this was available as QgsLayerTreeModelLegendNode.LegendNodeRoles\n\n.. versionadded:: 3.36\n\n" + '* ``RuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.RuleKey.__doc__ + '\n' + '* ``ParentRuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.ParentRuleKey.__doc__ + '\n' + '* ``NodeTypeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.NodeType.__doc__ +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole = QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize +QgsLayerTreeModelLegendNode.LegendNodeRoles.IsDataDefinedSizeRole = QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole.is_monkey_patched = True +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole.__doc__ = "Set when a node is related to data defined size (title or separated legend items). Added in 3.38" +QgsLayerTreeModelLegendNode.CustomRole.__doc__ = "Legend node data roles\n\n.. note::\n\n Prior to QGIS 3.36 this was available as QgsLayerTreeModelLegendNode.LegendNodeRoles\n\n.. versionadded:: 3.36\n\n" + '* ``RuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.RuleKey.__doc__ + '\n' + '* ``ParentRuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.ParentRuleKey.__doc__ + '\n' + '* ``NodeTypeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.NodeType.__doc__ + '\n' + '* ``IsDataDefinedSizeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize.__doc__ # -- QgsLayerTreeModelLegendNode.CustomRole.baseClass = QgsLayerTreeModelLegendNode QgsLayerTreeModelLegendNode.SimpleLegend = QgsLayerTreeModelLegendNode.NodeTypes.SimpleLegend diff --git a/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index 22ce85d34b1c..4b4f0cc98dde 100644 --- a/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -51,6 +51,7 @@ and customized look. RuleKey, ParentRuleKey, NodeType, + IsDataDefinedSize, }; enum NodeTypes /BaseType=IntEnum/ diff --git a/python/PyQt6/core/auto_generated/symbology/qgslegendsymbolitem.sip.in b/python/PyQt6/core/auto_generated/symbology/qgslegendsymbolitem.sip.in index 9df88ad6acdd..5228b39f8f23 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgslegendsymbolitem.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgslegendsymbolitem.sip.in @@ -110,6 +110,24 @@ Takes ownership of the settings object. QgsDataDefinedSizeLegend *dataDefinedSizeLegendSettings() const; %Docstring Returns extra information for data-defined size legend rendering. Normally it returns ``None``. +%End + + void setUserData( int key, QVariant &value ); +%Docstring +Adds a ``key`` - ``value`` pair to the item's user data. + +.. seealso:: :py:func:`userData` + +.. versionadded:: 3.38 +%End + + QVariant userData( int key ) const; +%Docstring +Retrieves the item's user data with the specified ``key``. + +.. seealso:: :py:func:`setUserData` + +.. versionadded:: 3.38 %End }; diff --git a/python/core/auto_additions/qgslayertreemodellegendnode.py b/python/core/auto_additions/qgslayertreemodellegendnode.py index 53d16ec17ec8..c165ff7ca078 100644 --- a/python/core/auto_additions/qgslayertreemodellegendnode.py +++ b/python/core/auto_additions/qgslayertreemodellegendnode.py @@ -13,6 +13,10 @@ QgsLayerTreeModelLegendNode.LegendNodeRoles.NodeTypeRole = QgsLayerTreeModelLegendNode.CustomRole.NodeType QgsLayerTreeModelLegendNode.NodeTypeRole.is_monkey_patched = True QgsLayerTreeModelLegendNode.NodeTypeRole.__doc__ = "Type of node. Added in 3.16" -QgsLayerTreeModelLegendNode.CustomRole.__doc__ = "Legend node data roles\n\n.. note::\n\n Prior to QGIS 3.36 this was available as QgsLayerTreeModelLegendNode.LegendNodeRoles\n\n.. versionadded:: 3.36\n\n" + '* ``RuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.RuleKey.__doc__ + '\n' + '* ``ParentRuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.ParentRuleKey.__doc__ + '\n' + '* ``NodeTypeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.NodeType.__doc__ +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole = QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize +QgsLayerTreeModelLegendNode.LegendNodeRoles.IsDataDefinedSizeRole = QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole.is_monkey_patched = True +QgsLayerTreeModelLegendNode.IsDataDefinedSizeRole.__doc__ = "Set when a node is related to data defined size (title or separated legend items). Added in 3.38" +QgsLayerTreeModelLegendNode.CustomRole.__doc__ = "Legend node data roles\n\n.. note::\n\n Prior to QGIS 3.36 this was available as QgsLayerTreeModelLegendNode.LegendNodeRoles\n\n.. versionadded:: 3.36\n\n" + '* ``RuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.RuleKey.__doc__ + '\n' + '* ``ParentRuleKeyRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.ParentRuleKey.__doc__ + '\n' + '* ``NodeTypeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.NodeType.__doc__ + '\n' + '* ``IsDataDefinedSizeRole``: ' + QgsLayerTreeModelLegendNode.CustomRole.IsDataDefinedSize.__doc__ # -- QgsLayerTreeModelLegendNode.CustomRole.baseClass = QgsLayerTreeModelLegendNode diff --git a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index 7b09fee9f685..2fc28ca3d363 100644 --- a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -51,6 +51,7 @@ and customized look. RuleKey, ParentRuleKey, NodeType, + IsDataDefinedSize, }; enum NodeTypes diff --git a/python/core/auto_generated/symbology/qgslegendsymbolitem.sip.in b/python/core/auto_generated/symbology/qgslegendsymbolitem.sip.in index 9df88ad6acdd..5228b39f8f23 100644 --- a/python/core/auto_generated/symbology/qgslegendsymbolitem.sip.in +++ b/python/core/auto_generated/symbology/qgslegendsymbolitem.sip.in @@ -110,6 +110,24 @@ Takes ownership of the settings object. QgsDataDefinedSizeLegend *dataDefinedSizeLegendSettings() const; %Docstring Returns extra information for data-defined size legend rendering. Normally it returns ``None``. +%End + + void setUserData( int key, QVariant &value ); +%Docstring +Adds a ``key`` - ``value`` pair to the item's user data. + +.. seealso:: :py:func:`userData` + +.. versionadded:: 3.38 +%End + + QVariant userData( int key ) const; +%Docstring +Retrieves the item's user data with the specified ``key``. + +.. seealso:: :py:func:`setUserData` + +.. versionadded:: 3.38 %End }; diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index b147059e98d8..6f0b1454fdcd 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -1301,7 +1301,8 @@ QList QgsLayerTreeModel::filterLegendNodes( const { for ( QgsLayerTreeModelLegendNode *node : std::as_const( nodes ) ) { - switch ( node->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::NodeType ) ).value() ) + const QgsLayerTreeModelLegendNode::NodeTypes nodeType = node->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::NodeType ) ).value(); + switch ( nodeType ) { case QgsLayerTreeModelLegendNode::EmbeddedWidget: filtered << node; @@ -1316,6 +1317,7 @@ QList QgsLayerTreeModel::filterLegendNodes( const case QgsLayerTreeModelLegendNode::ColorRampLegend: { const QString ruleKey = node->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::RuleKey ) ).toString(); + const bool isDataDefinedSize = node->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::IsDataDefinedSize ) ).toBool(); const bool checked = ( mFilterSettings && !( mFilterSettings->flags() & Qgis::LayerTreeFilterFlag::SkipVisibilityCheck ) ) || node->data( Qt::CheckStateRole ).toInt() == Qt::Checked; @@ -1324,7 +1326,11 @@ QList QgsLayerTreeModel::filterLegendNodes( const if ( QgsVectorLayer *vl = qobject_cast( node->layerNode()->layer() ) ) { auto it = mHitTestResults.constFind( vl->id() ); - if ( it != mHitTestResults.constEnd() && it->contains( ruleKey ) ) + if ( it != mHitTestResults.constEnd() && + ( it->contains( ruleKey ) || + ( !it->isEmpty() && isDataDefinedSize ) + ) + ) { filtered << node; } diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h index 39385684af37..7f3bed0e3856 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.h +++ b/src/core/layertree/qgslayertreemodellegendnode.h @@ -92,6 +92,7 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject RuleKey SIP_MONKEYPATCH_COMPAT_NAME( RuleKeyRole ) = Qt::UserRole, //!< Rule key of the node (QString) ParentRuleKey SIP_MONKEYPATCH_COMPAT_NAME( ParentRuleKeyRole ), //!< Rule key of the parent legend node - for legends with tree hierarchy (QString). Added in 2.8 NodeType SIP_MONKEYPATCH_COMPAT_NAME( NodeTypeRole ), //!< Type of node. Added in 3.16 + IsDataDefinedSize SIP_MONKEYPATCH_COMPAT_NAME( IsDataDefinedSizeRole ), //!< Set when a node is related to data defined size (title or separated legend items). Added in 3.38 }; Q_ENUM( CustomRole ) // *INDENT-ON* diff --git a/src/core/qgsdatadefinedsizelegend.cpp b/src/core/qgsdatadefinedsizelegend.cpp index d9420824c772..c8b2119a6d8e 100644 --- a/src/core/qgsdatadefinedsizelegend.cpp +++ b/src/core/qgsdatadefinedsizelegend.cpp @@ -15,6 +15,7 @@ #include "qgsdatadefinedsizelegend.h" +#include "qgslayertreemodellegendnode.h" #include "qgsproperty.h" #include "qgspropertytransformer.h" #include "qgssymbollayerutils.h" @@ -124,9 +125,11 @@ void QgsDataDefinedSizeLegend::updateFromSymbolAndProperty( const QgsMarkerSymbo QgsLegendSymbolList QgsDataDefinedSizeLegend::legendSymbolList() const { QgsLegendSymbolList lst; + QVariant isDataDefinedSize( true ); if ( !mTitleLabel.isEmpty() ) { QgsLegendSymbolItem title( nullptr, mTitleLabel, QString() ); + title.setUserData( static_cast( QgsLayerTreeModelLegendNode::CustomRole::IsDataDefinedSize ), isDataDefinedSize ); lst << title; } @@ -136,6 +139,7 @@ QgsLegendSymbolList QgsDataDefinedSizeLegend::legendSymbolList() const { QgsLegendSymbolItem i; i.setDataDefinedSizeLegendSettings( new QgsDataDefinedSizeLegend( *this ) ); + i.setUserData( static_cast( QgsLayerTreeModelLegendNode::CustomRole::IsDataDefinedSize ), isDataDefinedSize ); lst << i; break; } @@ -146,6 +150,7 @@ QgsLegendSymbolList QgsDataDefinedSizeLegend::legendSymbolList() const for ( const SizeClass &cl : mSizeClasses ) { QgsLegendSymbolItem si( mSymbol.get(), cl.label, QString() ); + si.setUserData( static_cast( QgsLayerTreeModelLegendNode::CustomRole::IsDataDefinedSize ), isDataDefinedSize ); QgsMarkerSymbol *s = static_cast( si.symbol() ); double size = cl.size; if ( mSizeScaleTransformer ) diff --git a/src/core/symbology/qgslegendsymbolitem.cpp b/src/core/symbology/qgslegendsymbolitem.cpp index d213cc396a4c..ef545020fef5 100644 --- a/src/core/symbology/qgslegendsymbolitem.cpp +++ b/src/core/symbology/qgslegendsymbolitem.cpp @@ -59,6 +59,7 @@ QgsLegendSymbolItem &QgsLegendSymbolItem::operator=( const QgsLegendSymbolItem & mScaleMaxDenom = other.mScaleMaxDenom; mLevel = other.mLevel; mParentKey = other.mParentKey; + mUserData = other.mUserData; return *this; } @@ -93,3 +94,13 @@ QgsDataDefinedSizeLegend *QgsLegendSymbolItem::dataDefinedSizeLegendSettings() c { return mDataDefinedSizeLegendSettings; } + +void QgsLegendSymbolItem::setUserData( int key, QVariant &value ) +{ + mUserData.insert( key, value ); +} + +QVariant QgsLegendSymbolItem::userData( int key ) const +{ + return mUserData.value( key, QVariant() ); +} diff --git a/src/core/symbology/qgslegendsymbolitem.h b/src/core/symbology/qgslegendsymbolitem.h index 9a3225f19438..59084878a565 100644 --- a/src/core/symbology/qgslegendsymbolitem.h +++ b/src/core/symbology/qgslegendsymbolitem.h @@ -108,6 +108,22 @@ class CORE_EXPORT QgsLegendSymbolItem */ QgsDataDefinedSizeLegend *dataDefinedSizeLegendSettings() const; + /** + * Adds a \a key - \a value pair to the item's user data. + * + * \see userData() + * \since QGIS 3.38 + */ + void setUserData( int key, QVariant &value ); + + /** + * Retrieves the item's user data with the specified \a key. + * + * \see setUserData() + * \since QGIS 3.38 + */ + QVariant userData( int key ) const; + private: //! Legend symbol -- may be NULLPTR. QgsSymbol *mSymbol = nullptr; @@ -135,6 +151,8 @@ class CORE_EXPORT QgsLegendSymbolItem int mLevel = 0; //! Key of the parent legend node. For legends with tree hierarchy QString mParentKey; + + QMap< int, QVariant > mUserData; }; typedef QList< QgsLegendSymbolItem > QgsLegendSymbolList; diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index 953a1ab91aff..86f4e01c6bec 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -199,6 +199,9 @@ class TestQgsLegendRenderer : public QgsTest void testDiagramAttributeLegend(); void testDiagramSizeLegend(); void testDataDefinedSizeCollapsed(); + void testDataDefinedSizeSeparated(); + void testDataDefinedSizeCollapsedFilterByMap(); + void testDataDefinedSizeSeparatedFilterByMap(); void testTextOnSymbol(); void testColumnsMixedSymbolSize(); @@ -1544,6 +1547,209 @@ void TestQgsLegendRenderer::testDataDefinedSizeCollapsed() delete root; } +void TestQgsLegendRenderer::testDataDefinedSizeSeparated() +{ + const QString testName = QStringLiteral( "legend_data_defined_size_separated" ); + + QgsVectorLayer *vlDataDefinedSize = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "Point Layer" ), QStringLiteral( "memory" ) ); + { + QgsVectorDataProvider *pr = vlDataDefinedSize->dataProvider(); + QList attrs; + attrs << QgsField( QStringLiteral( "test_attr" ), QMetaType::Type::Int ); + pr->addAttributes( attrs ); + + QgsFields fields; + fields.append( attrs.back() ); + + const QgsGeometry g = QgsGeometry::fromPointXY( QgsPointXY( 1.0, 1.0 ) ); + + QList features; + QgsFeature f1( fields, 1 ); + f1.setAttribute( 0, 100 ); + f1.setGeometry( g ); + QgsFeature f2( fields, 2 ); + f2.setAttribute( 0, 200 ); + f2.setGeometry( g ); + QgsFeature f3( fields, 3 ); + f3.setAttribute( 0, 300 ); + f3.setGeometry( g ); + features << f1 << f2 << f3; + pr->addFeatures( features ); + vlDataDefinedSize->updateFields(); + } + + QVariantMap props; + props[QStringLiteral( "name" )] = QStringLiteral( "circle" ); + props[QStringLiteral( "color" )] = QStringLiteral( "200,200,200" ); + props[QStringLiteral( "outline_color" )] = QStringLiteral( "0,0,0" ); + QgsMarkerSymbol *symbol = QgsMarkerSymbol::createSimple( props ); + QgsProperty ddsProperty = QgsProperty::fromField( QStringLiteral( "test_attr" ) ); + ddsProperty.setTransformer( new QgsSizeScaleTransformer( QgsSizeScaleTransformer::Linear, 100, 300, 10, 30 ) ); // takes ownership + symbol->setDataDefinedSize( ddsProperty ); + + QgsDataDefinedSizeLegend *ddsLegend = new QgsDataDefinedSizeLegend(); + ddsLegend->setLegendType( QgsDataDefinedSizeLegend::LegendSeparated ); + ddsLegend->setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) ); + + QgsSingleSymbolRenderer *r = new QgsSingleSymbolRenderer( symbol ); // takes ownership + r->setDataDefinedSizeLegend( ddsLegend ); + vlDataDefinedSize->setRenderer( r ); + + QgsLayerTree *root = new QgsLayerTree(); + root->addLayer( vlDataDefinedSize ); + + QgsLayerTreeModel legendModel( root ); + + QgsLegendSettings settings; + _setStandardTestFont( settings, QStringLiteral( "Bold" ) ); + QImage res = _renderLegend( &legendModel, settings ); + QVERIFY( _verifyImage( res, testName ) ); + + delete root; +} + +void TestQgsLegendRenderer::testDataDefinedSizeCollapsedFilterByMap() +{ + const QString testName = QStringLiteral( "legend_data_defined_size_filter_by_map" ); + + QgsVectorLayer *vlDataDefinedSize = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "Point Layer" ), QStringLiteral( "memory" ) ); + { + QgsVectorDataProvider *pr = vlDataDefinedSize->dataProvider(); + QList attrs; + attrs << QgsField( QStringLiteral( "test_attr" ), QMetaType::Type::Int ); + pr->addAttributes( attrs ); + + QgsFields fields; + fields.append( attrs.back() ); + + const QgsGeometry g = QgsGeometry::fromPointXY( QgsPointXY( 1.0, 1.0 ) ); + + QList features; + QgsFeature f1( fields, 1 ); + f1.setAttribute( 0, 100 ); + f1.setGeometry( g ); + QgsFeature f2( fields, 2 ); + f2.setAttribute( 0, 200 ); + f2.setGeometry( g ); + QgsFeature f3( fields, 3 ); + f3.setAttribute( 0, 300 ); + f3.setGeometry( g ); + features << f1 << f2 << f3; + pr->addFeatures( features ); + vlDataDefinedSize->updateFields(); + } + + QVariantMap props; + props[QStringLiteral( "name" )] = QStringLiteral( "circle" ); + props[QStringLiteral( "color" )] = QStringLiteral( "200,200,200" ); + props[QStringLiteral( "outline_color" )] = QStringLiteral( "0,0,0" ); + QgsMarkerSymbol *symbol = QgsMarkerSymbol::createSimple( props ); + QgsProperty ddsProperty = QgsProperty::fromField( QStringLiteral( "test_attr" ) ); + ddsProperty.setTransformer( new QgsSizeScaleTransformer( QgsSizeScaleTransformer::Linear, 100, 300, 10, 30 ) ); // takes ownership + symbol->setDataDefinedSize( ddsProperty ); + + QgsDataDefinedSizeLegend *ddsLegend = new QgsDataDefinedSizeLegend(); + ddsLegend->setLegendType( QgsDataDefinedSizeLegend::LegendCollapsed ); + ddsLegend->setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) ); + + QgsSingleSymbolRenderer *r = new QgsSingleSymbolRenderer( symbol ); // takes ownership + r->setDataDefinedSizeLegend( ddsLegend ); + vlDataDefinedSize->setRenderer( r ); + + QgsLayerTree *root = new QgsLayerTree(); + root->addLayer( vlDataDefinedSize ); + + QgsLayerTreeModel legendModel( root ); + + QgsMapSettings mapSettings; + // extent and size to include only the red and green points + mapSettings.setExtent( QgsRectangle( 10, 10, 20, 20 ) ); + mapSettings.setOutputSize( QSize( 400, 100 ) ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setLayers( QgsProject::instance()->mapLayers().values() ); + + QgsLayerTreeFilterSettings filterSettings( mapSettings ); + legendModel.setFilterSettings( &filterSettings ); + + QgsLegendSettings settings; + _setStandardTestFont( settings, QStringLiteral( "Bold" ) ); + QImage res = _renderLegend( &legendModel, settings ); + QVERIFY( _verifyImage( res, testName ) ); + + delete root; +} + +void TestQgsLegendRenderer::testDataDefinedSizeSeparatedFilterByMap() +{ + const QString testName = QStringLiteral( "legend_data_defined_size_filter_by_map" ); + + QgsVectorLayer *vlDataDefinedSize = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "Point Layer" ), QStringLiteral( "memory" ) ); + { + QgsVectorDataProvider *pr = vlDataDefinedSize->dataProvider(); + QList attrs; + attrs << QgsField( QStringLiteral( "test_attr" ), QMetaType::Type::Int ); + pr->addAttributes( attrs ); + + QgsFields fields; + fields.append( attrs.back() ); + + const QgsGeometry g = QgsGeometry::fromPointXY( QgsPointXY( 1.0, 1.0 ) ); + + QList features; + QgsFeature f1( fields, 1 ); + f1.setAttribute( 0, 100 ); + f1.setGeometry( g ); + QgsFeature f2( fields, 2 ); + f2.setAttribute( 0, 200 ); + f2.setGeometry( g ); + QgsFeature f3( fields, 3 ); + f3.setAttribute( 0, 300 ); + f3.setGeometry( g ); + features << f1 << f2 << f3; + pr->addFeatures( features ); + vlDataDefinedSize->updateFields(); + } + + QVariantMap props; + props[QStringLiteral( "name" )] = QStringLiteral( "circle" ); + props[QStringLiteral( "color" )] = QStringLiteral( "200,200,200" ); + props[QStringLiteral( "outline_color" )] = QStringLiteral( "0,0,0" ); + QgsMarkerSymbol *symbol = QgsMarkerSymbol::createSimple( props ); + QgsProperty ddsProperty = QgsProperty::fromField( QStringLiteral( "test_attr" ) ); + ddsProperty.setTransformer( new QgsSizeScaleTransformer( QgsSizeScaleTransformer::Linear, 100, 300, 10, 30 ) ); // takes ownership + symbol->setDataDefinedSize( ddsProperty ); + + QgsDataDefinedSizeLegend *ddsLegend = new QgsDataDefinedSizeLegend(); + ddsLegend->setLegendType( QgsDataDefinedSizeLegend::LegendSeparated ); + ddsLegend->setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) ); + + QgsSingleSymbolRenderer *r = new QgsSingleSymbolRenderer( symbol ); // takes ownership + r->setDataDefinedSizeLegend( ddsLegend ); + vlDataDefinedSize->setRenderer( r ); + + QgsLayerTree *root = new QgsLayerTree(); + root->addLayer( vlDataDefinedSize ); + + QgsLayerTreeModel legendModel( root ); + + QgsMapSettings mapSettings; + // extent and size to include only the red and green points + mapSettings.setExtent( QgsRectangle( 10, 10, 20, 20 ) ); + mapSettings.setOutputSize( QSize( 400, 100 ) ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setLayers( QgsProject::instance()->mapLayers().values() ); + + QgsLayerTreeFilterSettings filterSettings( mapSettings ); + legendModel.setFilterSettings( &filterSettings ); + + QgsLegendSettings settings; + _setStandardTestFont( settings, QStringLiteral( "Bold" ) ); + QImage res = _renderLegend( &legendModel, settings ); + QVERIFY( _verifyImage( res, testName ) ); + + delete root; +} + void TestQgsLegendRenderer::testTextOnSymbol() { const QString testName = QStringLiteral( "legend_text_on_symbol" ); diff --git a/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map.png b/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map.png new file mode 100644 index 000000000000..a8408d321447 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map_mask.png b/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map_mask.png new file mode 100644 index 000000000000..3c7b5ca0f8fe Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_data_defined_size_filter_by_map/expected_legend_data_defined_size_filter_by_map_mask.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated.png b/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated.png new file mode 100644 index 000000000000..e740ffc29491 Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated.png differ diff --git a/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated_mask.png b/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated_mask.png new file mode 100644 index 000000000000..fdc0b947f40d Binary files /dev/null and b/tests/testdata/control_images/legend/expected_legend_data_defined_size_separated/expected_legend_data_defined_size_separated_mask.png differ