3

Background: I'm currently trying to dynamically change the ranges for a layer with new style graduated symbology.

Problem: I thought it may be as simple as updating the ranges for the existing symbology using the setLowerValue() and setUpperValue() methods on the QgsRendererRangeV2 class as below:

layer = qgis.util.iface.activeLayer()
rangePoint = 0
rangeIncrement = 100
for range in layer.rendererV2().ranges():
        range.setLowerValue(rangePoint)
        rangePoint += rangeIncrement
        range.setUpperValue(rangePoint)

By my reconning the API documentation suggests that this should be possible.

However, I am getting the error:

AttributeError: 'QgsRendererRangeV2' object has no attribute 'setLowerValue'

I've tried also using:

range.setLowerValue(float(rangePoint))

with no luck.

What am I doing wrong?

5
  • Are you using 1.8?
    – Nathan W
    Commented Oct 17, 2012 at 5:24
  • Sorry, yes, 1.8.
    – Jayden
    Commented Oct 17, 2012 at 6:22
  • 2
    I suspect there might be missing Python binding for that class.
    – Nathan W
    Commented Oct 17, 2012 at 7:27
  • I'm able to call the lowerValue() method and it returns a value, I'm guessing this would suggest that the binding is there and exists?
    – Jayden
    Commented Oct 17, 2012 at 8:49
  • no because setLowerValue() is a different method and could be missing the binding.
    – Nathan W
    Commented Oct 17, 2012 at 8:51

3 Answers 3

1

I haven't been able to figure out how to fix the bindings if this was indeed the cause, I've used a work around, which was to follow the approach presented by pyqgis-cookbook, and to remove the existing ranges / symbology and re-create new ones (based on the existing ones). This approach now works for me. I'm not sure how efficient it is, but with the data set I'm currently using, it's quick enough.

from PyQt4.QtGui import QColor              # import this namespace as I want to refer to QColor later
from PyQt4.QtCore import QVariant

def satramp(hue):
    layer = qgis.utils.iface.activeLayer()
    grads = len(layer.rendererV2().symbols())
    vinc = 0
    val = 180
    i = 0 - (255 / grads)                                       # used to increment the saturation value
    j = val - (grads * vinc)
    for sym in layer.rendererV2().symbols():        # loop through each symbol in the layer using 'new symbology'
        i += (255 / grads)
        j += vinc
        sym.setColor(QColor.fromHsv(hue,i,j))
    canvas = qgis.utils.iface.mapCanvas()
    canvas.refresh()

def rap():
    canvas = qgis.utils.iface.mapCanvas()
    layer = qgis.utils.iface.activeLayer()
    layer.reload()
    canvas.refresh()

# gets the layer for the passed index value based on the legend index
def getLayerByIndex(layerIndex):
    idx = 0
    for layer in qgis.utils.iface.legendInterface().layers():
        if idx == layerIndex:
            break
        idx += 1
    return layer

# gets the field index for the given field name
def getFieldIndex(fieldName,layer):
    mFieldMap = {}
    fields = layer.pendingFields()
    for ( key, field ) in fields.iteritems():
        if field.type() == QVariant.Int or field.type() == QVariant.Double:
            if field.name() == fieldName:
                return key

# sets a saturation ramp for a graduated layer only, based on the passed hue, value and index of the layer from the legend
def satramp(layerindex, hue, value):
    layer = getLayerByIndex(layerindex)
    grads = len(layer.rendererV2().symbols())                   # determine the number of graduations to use based on 
    vinc = 0                                                    # value incrementer, currently not used, but can be adjusted to increment the value also
    val = value                                                 
    i = 0 - (255 / grads)                                       # used to increment the saturation value
    j = val - (grads * vinc)
    layer.reload()                                              # refresh the data source

    # determine range increments
    provider = layer.dataProvider()
    fieldName = layer.rendererV2().classAttribute()
    fieldIndex = getFieldIndex(fieldName,layer)
    min = provider.minimumValue(fieldIndex).toDouble()[ 0 ]     # get the min and max values to calculate the new gradations
    max = provider.maximumValue(fieldIndex).toDouble()[ 0 ]
    rangeCount = len(layer.rendererV2().ranges())
    rangeIncrement = (max - min) / rangeCount
    rangePoint = min

    # construt a new renderer using ranges based on the existing renderer
    rangeList = []  
    for range in layer.rendererV2().ranges():
        newSymbol = QgsSymbolV2.defaultSymbol(layer.geometryType())
        label = str(rangePoint) + " - " + str(rangePoint + rangeIncrement)
        newRange = QgsRendererRangeV2(rangePoint,rangePoint + rangeIncrement, newSymbol, label)
        rangeList.append(newRange)
        rangePoint += rangeIncrement

    newRenderer = QgsGraduatedSymbolRendererV2('',rangeList)
    newRenderer.setClassAttribute(layer.rendererV2().classAttribute())
    newRenderer.setMode(QgsGraduatedSymbolRendererV2.EqualInterval)
    layer.setRendererV2(newRenderer)

    for sym in layer.rendererV2().symbols():        # loop through each symbol in the layer using 'new symbology'
        i += (255 / grads)
        j += vinc
        sym.setColor(QColor.fromHsv(hue,i,j))

    qgis.utils.iface.legendInterface().refreshLayerSymbology(layer)
    canvas = qgis.utils.iface.mapCanvas()
    canvas.refresh()
1
  • PS: I know my code isn't very robust and doesn't check appropriate pre-conditions, like ensuring the layer is actually a vector layer with a new graduated symbology - but for now, this meets my need without this additional consideration.
    – Jayden
    Commented Oct 17, 2012 at 20:40
0

First of all, checking the QGIS API doc, I can see that the ranges method from the QgsGraduatedSymbolRendererV2 class returns a

const QgsRangeList &

probably meaning that you have little chance to modify its return value.

Additionally, when looping the way you do in Python, you cannot modify a list's values as explained here.

=> twice impossible :)

Fortunately, the solution directly comes from the QgsGraduatedSymbolRendererV2 class thanks to :

renderer.updateRangeUpperValue(iRangeIndex,value)
renderer.updateRangeLowerValue(iRangeIndex,value)

The binding works correctly on my version (QGIS 2.8.2). The final code would look like this:

limit = 0
step = 100
for i, range in enumerate(renderer.ranges()):
    renderer.updateRangeLowerValue(i,limit)
    renderer.updateRangeUpperValue(i,limit+step)
    limit+=step
0

Bringing this back as I have found a solution that some may be able to use. This seemed harder than it should be but I don't think the functionality exists yet for vector features. It uses python to run in the QGIS console and refresh the symbology of a layer via the min-max of a selected field, everytime the user zooms or pans the canvas. Layer name and attribute is defined at the beginning of the script. Set continue_script to 0 and run again to pause the update of symbology. Basic functionality but useful for some tasks.

from qgis.core import QgsProject, QgsFeatureRequest, QgsGraduatedSymbolRenderer, QgsRendererRange, QgsSymbol
from qgis.PyQt.QtGui import QColor

# Flag to control whether the script should continue running
continue_script = 1
#continue_script = 0" #this pauses the script
layer_name= 'name_of_your_layer'
field = 'attribute_name_to_be_used_for_range'

def update_graduated_symbology(layer_name):
    global continue_script  # Use the global flag
    if continue_script == 0:
        print("Script stopped by user.")
        return
    all_layers = QgsProject.instance().mapLayers().values()
    layer = None
    for l in all_layers:
        if l.name() == layer_name:
            layer = l
            break
    if layer is None:
        print(f"Layer {layer_name} not found.")
        return
    
    # Get features within the current map extent
    extent = iface.mapCanvas().extent()
    request = QgsFeatureRequest().setFilterRect(extent)
    features = layer.getFeatures(request)
    
    min_val = float('inf')
    max_val = float('-inf')
    for feature in features:
        val = feature[field]
        min_val = min(min_val, val)
        max_val = max(max_val, val)
        
    # Fetch the existing renderer
    renderer = layer.renderer()
    if renderer.type() == 'graduatedSymbol':
        try:
            colors = [
            QColor(255, 224, 224),  # Very light red
            QColor(255, 160, 160), # Lighter red
            QColor(255, 0, 0),   # Red
            QColor(177, 0, 0),   # Slightly less dark red
            QColor(20, 0, 0)    # Darker red
            ]
            # Create 5 QgsRendererRange objects with different colors
            ranges = []
            interval = (max_val - min_val) / 5

            # Get source symbol or create a new default one if it's None
            source_symbol = renderer.sourceSymbol()
            if source_symbol is None:
                source_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
            
            for i in range(5):
                lower = min_val + i * interval
                upper = lower + interval
                
                # Clone the source symbol
                symbol = source_symbol.clone()
                
                # Set the color
                symbol.setColor(colors[i])

                # Set the stroke color to be 20% darker than the fill color
                if symbol.symbolLayerCount() > 0:
                    symbol_layer = symbol.symbolLayer(0)
                    if hasattr(symbol_layer, 'setStrokeColor'):
                        darker_stroke_color = colors[i].darker(120)
                        symbol_layer.setStrokeColor(darker_stroke_color)

                #sets the color names with the actual values
                new_range = QgsRendererRange(lower, upper, symbol, f'{lower} - {upper}')  # Updated this line
                ranges.append(new_range)

            new_renderer = QgsGraduatedSymbolRenderer(field, ranges)
            layer.setRenderer(new_renderer)
            
            # Refresh the layer symbology in the layer tree
            iface.layerTreeView().refreshLayerSymbology(layer.id())
            
        except Exception as e:
            ''
            #print(f"Error while setting renderer: {e}")  # Debugging
    
    # Explicitly trigger a refresh
    layer.triggerRepaint()


# Function to stop the script
def stop_script():
    global continue_script
    continue_script = 0
    print("Script will be stopped.")

# Function to restart the script
def restart_script():
    global continue_script
    continue_script = 1
    print("Script will continue.")

# Connect the function to the extentsChanged signal
iface.mapCanvas().extentsChanged.connect(lambda: update_graduated_symbology(layer_name))

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.