A very interesting questions, that no doubt is beyond built-in functionality. I would split this problem in to 3 parts.
To go from your data to a structure that is appropriate for a rule-based tree you need some additional information in your data. If you don't apply more information for each feature, the first rule will have to hold information for all features belonging to that main-branch, and QGIS can't handle that.
A suggestion would be to go from your structure with one-level information to tree-depth information on all nodes, such as this:
Now all features can be identified without searching neighboring features.
I kept the code a iterative and modular as possible since that (possibly) makes it easier to reuse.
layer = iface.activeLayer()
#first section finds all the super-parents. Features that are root.
#By having super-parents we can iterate down the featurelist with a starting-point
#get all features in layer
features = layer.getFeatures()
#placeholder list
itemList = []
#Add all features to our placeholderlist
for ele in features:
itemList.append(ele.attributes())
fidList=[e[0] for e in itemList]
super_parent_features = [e for e in itemList if e[1] not in fidList]
super_parent_features_fid = [e[0] for e in super_parent_features]
for e in itemList:
if e[0] in super_parent_features_fid:
e.append(0)
#itemlist now contains all superparents, which is our point of comparison
#Function that adds tree information to unassigned items by comparing with the level above
#Intentionally readability over performance
def returnParentLvl(candidates, parents, lvl):
parents_fid = [e[0] for e in parents if len(e) == 4 and e[3] == lvl-1]
for idx,e in enumerate(candidates):
if e[1] in parents_fid:
e.append(lvl)
return(candidates, parents, lvl)
#the call below will loop the featurelist
lvl=1
while len([e for e in itemList if len(e) == 3])>0:
#list that holds all items that hasn't been assigned a parent
unassignedItems = [e for e in itemList if len(e) == 3]
#adds parental information - the number of iterations must equal the level in the hierachy
returnParentLvl(unassignedItems, itemList,lvl)
lvl+=1
#adds information to table
layer.dataProvider().addAttributes([QgsField(f"lvl_in_hiercahy", QVariant.Int)])
layer.updateFields()
itemList.sort(key = lambda itemList: itemList[3])
lowestLvl=max([e[3] for e in itemList])
#adds grandparents to features and add column to hold ref.value
#Insert a column called lvl_in_hiercahy
for i in range(0,lowestLvl,1):
layer.dataProvider().addAttributes([QgsField(f"lvl{i}", QVariant.String, '', 3)])
layer.updateFields()
for parent in itemList:
if parent[3] == i:
for child in itemList:
if child[3] == i+1:
if parent[0]==child[1]:
try:
for grandparents in parent[4:]:
child.append(grandparents)
except:
pass
child.append(parent[1])
#Fill out table
for item in itemList:
iterator = layer.getFeatures(QgsFeatureRequest().setFilterExpression(f""""fid"={item[0]}"""))
attr_value={3:int(item[3])}
feat = list(iterator)[0]
id=feat.id()
layer.dataProvider().changeAttributeValues({id:attr_value})
#the 4 is a mess - it means that the iterator ignores the first 4 columns
for i in range(4,lowestLvl+4,1):
try:
attr_lvl_value={i:item[i]}
layer.dataProvider().changeAttributeValues({id:attr_lvl_value})
except:
pass
layer.updateFields()
layer.triggerRepaint()
There are many simpler approaches, but I find that level could be suitable.
This is sort of a flat tree. You can know create a rule-set that is simple enough for QGIS, where each level of rules is just a look-up in a single column. During testing I tried to create rule-set that would encapsulate all 'fid's' for each level, and it crashed the system.
To apply a nested ruleset you can do 2 processes. Firstly create a tree by making a tree-class. Then iterate the tree and apply a nesting-level suitable for the lvl_in_hierachy.
To create the rules, with the right nesting and as few refinements as possible (to move the lifting from the renderer to the script), we can now look only two places. The variable indicating the place in the hiercahy, and the nearest parent.
The code could look something like this . which should result in the image :
from qgis.PyQt.QtGui import QColor
from qgis.core import QgsRuleBasedRenderer, QgsSymbol, QgsFillSymbol, QgsSimpleMarkerSymbolLayer
from qgis.utils import iface
layer = iface.activeLayer()
# Define a class called "TreeNode"
class TreeNode:
# Initialize the class with attributes: "data" and "children" and "lvl"
def __init__(self, data, lvlno):
self.data = data
self.children = []
self.rule = None
self.lvlno = 0
# Create a new instance of the TreeNode class and assign it to the variable "root"
root = TreeNode(None,0)
# Define a function to recursively create rules from a given node and add them to a parent rule
def create_rules(node, parent_rule):
# Create a new rule for this branchlevel
symbol = QgsMarkerSymbol.createSimple({})
symbol.setColor(QColor.fromHsl((node.lvlno * 60) % 360, 255, 128))
symbol.setSize(2)
rule = QgsRuleBasedRenderer.Rule(symbol)
# Set the label and filter expression for the rule
label = f'{node.data}'
rule.setLabel(label)
if node.data is None:
rule.setFilterExpression(f""""lvl0" is not NULL""")
else:
rule.setFilterExpression(f""""lvl{node.lvlno}"='{node.data}'""")
# Add the rule to the parent rule and set it as the node's rule
parent_rule.appendChild(rule)
node.rule = rule
# Recursively create rules for each child node and add them to this rule
for child in node.children:
create_rules(child, rule)
features = layer.getFeatures()
#Add all features to our placeholderlist
itemHold=[]
for feature in features:
itemTemp=[]
for c,element in enumerate(feature):
if element != NULL and c > 3:
itemTemp.append(element)
itemHold.append(itemTemp)
itemSet = itemHold
# Populate the tree with the items in itemSet
for item in itemSet:
current_node = root
lvlno=0
for element in item:
found_child = False
for child in current_node.children:
if child.data == element:
current_node = child
found_child = True
break
if not found_child:
new_child = TreeNode(element, lvlno)
new_child.lvlno = lvlno
current_node.children.append(new_child)
current_node = new_child
#lvl is determined by the iteration in which the node is set
lvlno+=1
# Create the root rule and recursively create rules for the tree
symbol = QgsFillSymbol.createSimple({})
symbol.setColor(QColor.fromHsl(0, 0, 255))
root_rule = QgsRuleBasedRenderer.Rule(symbol)
root_rule.setLabel('Root Rule')
rb_renderer = QgsRuleBasedRenderer(root_rule)
create_rules(root, root_rule)
# Assign the created renderer to the layer
if rb_renderer is not None:
layer.setRenderer(rb_renderer)
layer.triggerRepaint()
If you want the process to run by itself when you add/alter the layer you can put it in an action, or put the whole script (and a part that empties the tables) and run that manually on edits.