8

I am trying to find a way to remove specific legend nodes in a layout legend for a vector layer styled with categorized symbology using the PyQGIS-3 API. For example, in a QGIS project I have one categorized layer, a few grouped layers and a number of other ungrouped, vector and raster layers.

How can I remove one or more of the legend nodes belonging to the categorized layer using PyQGIS?

It is a trivial task to remove them manually via item properties in the layout interface but I can not find any way to accomplish this using PyQGIS. The layout legend (annotated with model rows, groups, parent indices and child node positions) looks like this:

enter image description here

If I access the legend model and get its row count like this:

d = iface.openLayoutDesigners()[0]
l = d.layout()
legend = l.selectedItems()[0]
m = legend.model()
print(m.rowCount())

the row count method returns 8 rows. This tells me that neither the category nodes, nor the child nodes of the group are considered to be separate rows in the model. However the legend nodes of the ungrouped layers and the child nodes of the grouped layers can be removed quite easily. First an ungrouped layer:

# remove rows with no parent
d = iface.openLayoutDesigners()[0]
l = d.layout()
legend = l.selectedItems()[0]
m = legend.model()
legend.setAutoUpdateModel(False)
m.removeRows(4, 1) # pass row index & number of rows to remove
legend.refresh()

Then a member of the ‘Collecting Targets’ group:

# remove rows from a group
d = iface.openLayoutDesigners()[0]
l = d.layout()
legend = l.selectedItems()[0]
m = legend.model()
idx = m.index(1, 0) # get index of parent
legend.setAutoUpdateModel(False)
m.removeRows(1, 1, idx) # pass position, number of rows to remove & index of parent
legend.refresh()

However, if I use the code above but get the index of the categorized layer and pass that to the removeRows() method as the 3rd argument (parent), no rows are removed. There is no error encountered- it just does not work.

I can use the method legendRootRowCount() to return each category of the categorized layer as a row.

I can also access the nodes of the categorized layer and store the parent and its nodes in a dictionary like so:

d = iface.openLayoutDesigners()[0]
l = d.layout()
legend = l.selectedItems()[0]
layer_and_nodes = dict()
m = legend.model()
idx = m.index(0, 0)
n = m.index2node(idx)
nodes = m.layerLegendNodes(n)
layer_and_nodes[n.name()] = nodes
print(layer_and_nodes)

This returns:

{'fs2019shp': [<qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809CFE58>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809CFEE8>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809CFF78>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809E2048>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809E20D8>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809E2168>, <qgis._core.QgsLayerTreeModelLegendNode object at 0x00000013809E21F8>]}

But I can’t work out any way of removing or filtering those QgsLayerTreeModelLegendNode objects.

I tried:

llns = [v for k, v in layer_and_nodes.items()][0]
m.filterLegendNodes(llns)

I suspect I am not implementing this method correctly as this also fails with no errors.

I try to force a refresh of layer nodes for the categorized layer after both the removeRows() and the filterLegendNodes() calls using m.refreshLayerLegend(n) which does not have any effect.

I have scoured the API documentation and searched GIS.StackExchange for similar questions. My research also introduced me to the concept of QT model/ view programming (of which I admit I was completely ignorant) but I have not been able to find a solution.

How can I remove a legend node of a categorized layer from a QgsLegendModel using PyQGIS?

1 Answer 1

11

I had to dig in QGIS source code to find how QGIS does it when one reorders or removes a legend node, but here it is.

It boils down to setting a custom property, then calling refresh on the parent layer node.

The custom property key is called 'legend/node-order' it can be

  • A comma separated list of valid legend nodes indexes (e.g '0,1,3' to display the first, second, and fourth category)
  • 'empty' : No legend node displayed. This is what you probably want
  • Any other value will just result in every legend node being displayed (eg.'foo', '0,299', '', ...)

QGIS provides a helper function QgsMapLayerLegendUtils.setLegendNodeOrder which takes a QgsLayerTreeLayer and a list of ints.

Do not forget to call model.refreshLayerLegend(layerNode) to take your changes into account.

Here are some helper functions

def set_visible_legend_nodes(layer, layout, indexes):
    # Get the legend item from layout
    legend = next(item for item in layout.items() if isinstance(item, QgsLayoutItemLegend))
    model = legend.model()
    # Get the layer node linked to input layer
    layerNode  = model.rootGroup().findLayer(layer)
    # Set the legend node order
    QgsMapLayerLegendUtils.setLegendNodeOrder(layerNode, indexes)
    # Refresh the legend
    model.refreshLayerLegend(layerNode)
    
def show_all_legend_nodes(layer, layout):
    # Invalid index: reset visible legend nodes
    set_visible_legend_nodes(layer, layout, [-1])
    
def hide_all_legend_nodes(layer, layout):
    # empty list, will translate into 'empty' in 'legend/node-order'
    set_visible_legend_nodes(layer, layout, [])

Example usage

>>> d = iface.openLayoutDesigners()[0]
>>> layout = d.layout()
>>> layer = iface.activeLayer()
>>> hide_all_legend_nodes(layer , layout)

For reference, here are the relevant source files:

  • ./src/app/layout/qgslayoutlegendwidget.cpp (see mRemoveToolButton_clicked)
  • ./src/core/qgsmaplayerlegend.cpp (see setLegendNodeOrder)
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.