10

I'm trying to apply a color ramp to a QgsGraduatedSymbolRenderer in PyQGIS 3 (QGIS 3.8) using the following code:

Getting the QgsGradientColorRamp:

myStyle = QgsStyle().defaultStyle()
defaultColorRampNames = myStyle.colorRampNames()
ramp = myStyle.colorRamp(defaultColorRampNames[-6]) #Spectral color ramp

Applying it to the QgsGraduatedSymbolRenderer:

myRenderer = QgsGraduatedSymbolRenderer('', myRangeList) #A list of break values
myRenderer.setSourceColorRamp(ramp)
myVectorLayer.setRenderer(myRenderer)
myVectorLayer.triggerRepaint()

Unfortunately the layer still shows in its original colors without the color map being applied. Upon looking in properties, however, oddly enough the color map is applied partially. The color map selected is indeed the one chosen ('Spectral'), but this isn't applied to the values' symbols. If I then just click on the color ramp and reapply it, it works, but I'd like to achieve this all through PyQGIS. I looked at similar questions here and the QGIS3 Python API and couldn't find a solution.

enter image description here

3
  • 1
    Hi 15Step, do you need to include the field name in the QgsGraduatedSymbolRenderer statement - it only seems to have empty single quotes at the moment?
    – ian
    Nov 19, 2019 at 21:17
  • You mention '#A list of break values', do you have any info about these? Have you specified these already?
    – Cushen
    Nov 20, 2019 at 23:26
  • So the variable myRangeList includes a list of QgsRendererRanges, instantiated with these variables: myRange = QgsRendererRange(myMin, myMax, mySymbol, myLabel). I basically followed the example from the PyQGIS cookbook here: docs.qgis.org/testing/pdf/en/…, on page 37.
    – 15Step
    Nov 21, 2019 at 9:15

5 Answers 5

11

Updated answer:

Below is a recipe to apply a graduated renderer based on values in an attribute field, specifying the number of classes and color ramp.

# Set layer name and desired paremeters
layer_name = 'Your_layer_name'
ramp_name = 'Spectral'
value_field = 'Your_field_name'
num_classes = 5
classification_method = QgsClassificationEqualInterval()

#You can use any of these classification method classes:
#QgsClassificationQuantile()
#QgsClassificationEqualInterval()
#QgsClassificationJenks()
#QgsClassificationPrettyBreaks()
#QgsClassificationLogarithmic()
#QgsClassificationStandardDeviation()

layer = QgsProject().instance().mapLayersByName(layer_name)[0]

# change format settings as necessary
format = QgsRendererRangeLabelFormat()
format.setFormat("%1 - %2")
format.setPrecision(2)
format.setTrimTrailingZeroes(True)

default_style = QgsStyle().defaultStyle()
color_ramp = default_style.colorRamp(ramp_name)

renderer = QgsGraduatedSymbolRenderer()
renderer.setClassAttribute(value_field)
renderer.setClassificationMethod(classification_method)
renderer.setLabelFormat(format)
renderer.updateClasses(layer, num_classes)
renderer.updateColorRamp(color_ramp)

layer.setRenderer(renderer)
layer.triggerRepaint()

I will leave the original answer below:

I have been through the same experience before, and (I may be wrong) but as far as I am aware, it is not possible to programmatically set a default color ramp and have it automatically applied. As you discovered, doing setSourceColorRamp() just sets that color ramp in the symbology dialog- it doesn't apply it to the renderer range symbols.

You have to create each range, instantiate QgsSymbol() objects, set colors to them and pass those in to a QgsRendererRange() constructor, together with the range and legend values.

It's a bit hacky, but I knocked out the script below which will create a graduated symbology with 10 classes (like your example above) in a basic spectral color ramp. Just change the layer name to match yours:

layer = QgsProject().instance().mapLayersByName('Your_Layer_Name')[0]
vals = []
fld = 'AREA'
for f in layer.getFeatures():
    vals.append(f[fld])
# If you don't like these colors, change them out for ones you do, using hexcodes,
# RGB codes etc. as long as the items in this list are valid strings you
# can pass to a QColor constructor 
colors = ['#0011FF', '#0061FF', '#00D4FF', '#00FF66', '#00FF00', '#E5FF32', '#FCFC0C', '#FF9F00', '#FF3F00', '#FF0000']
lower = sorted(vals)[0]
upper = sorted(vals)[-1]
step = (upper-lower)/len(colors)
range_list = []
for c in colors:
    cat = [lower, lower+step, c]
    sym = QgsSymbol.defaultSymbol(layer.geometryType())
    sym.setColor(QColor(cat[2]))
    rng = QgsRendererRange(cat[0], cat[1], sym, '{0:.1f}-{1:.1f}'.format(cat[0], cat[1]))
    range_list.append(rng)
    lower = (lower+step)+0.1
renderer = QgsGraduatedSymbolRenderer(fld, range_list)
layer.setRenderer(renderer)
layer.triggerRepaint()

Below is the result of this script run on a sample dataset- IBRA Bioregions of Australia, obtaining the graduation values from the "SHAPE_Area" field.

enter image description here

Based on the comment by @Cushen below, you could obtain a range of color values from an existing styled layer with a graduated ramp of the required number of classes and read those colors into a list object with a simple list comprehension:

styled_layer = iface.activeLayer() # existing layer styled with desired color ramp
colors = [s.color().name() for s in styled_layer.renderer().symbols(QgsRenderContext())]
3
  • Nice @Ben W. I notice it is possible to extract the colors from an existing Graduated ramp. Those colours could be extracted and fed into you 'colors' array; there will be a need to interpolate to find the middle colors as only the breakpoints seem to be included right now (i.e. if you want 5 steps but only 3 colours exist in the ramp). To get colors from a renderer: symbols = myLayer.renderer().symbols(QgsRenderContext()) for s in symbols: print (symbols[-1].color().name())
    – Cushen
    Nov 21, 2019 at 3:15
  • Thanks @Cushen. Good idea to obtain a range of color codes for the different class values from an existing layer. I just found a color chart online, copied the hex codes and stuck them into a list!
    – Ben W
    Nov 21, 2019 at 9:00
  • @BenW Very nice workaround, thx! It seems odd however that there isn't an easier way to do it following the 'official' examples from the PyQGIS cookbook. Also, the way to apply a different classification (equal intervals, natural breaks) described there also doesn't seem to work through PyQGIS, but that's a different question.
    – 15Step
    Nov 21, 2019 at 9:19
3

I've put together a script using bits from @Ben W's answer as well as from this answer this answer to a different question.

This takes the following inputs:

  • Number of Classes (number_classes)
  • Filed to use in the ramp (fld)
  • Existing Color Ramp (colorRampNumber)
    • Optional Alternative: user specified start and end colors to use for a custom color ramp

This generates and applies a color ramp to the specified field of active layer with the number of classes chosen.

The colors are taken from the start and end colors of the specified preexisting ramp, intermediate colors are 'interpolated' thanks to the colour library; this might need to be downloaded.
Alternativly the user can specify the start and end colors themselves (c1 and c2).

Code here:

from qgis.PyQt.QtGui import QColor
import re
import colour
from colour import Color

#to interpolate colors:
def linear_color_interpolation(c1, c2, t):
    r = int(c1.red() + (c2.red() - c1.red()) * t)
    g = int(c1.green() + (c2.green() - c1.green()) * t)
    b = int(c1.blue() + (c2.blue() - c1.blue()) * t)
    return QColor(r, g, b)

layer = iface.activeLayer()
renderer = layer.renderer()
provider = layer.dataProvider()

#Specify inputs:
fld = 'roof_heigh'
colorRampNumber = 4
number_classes = 5

#COLOR RAMP STYLE:
myStyle = QgsStyle().defaultStyle()
defaultColorRampNames = myStyle.colorRampNames()
ramp = myStyle.colorRamp(defaultColorRampNames[colorRampNumber]) #Spectral color ramp


vals = []
for f in layer.getFeatures():
    vals.append(f[fld])
vals = (list(filter(None,vals))) #REMOVE NONE/NULL from list

idx = provider.fieldNameIndex(fld)
max = layer.maximumValue(idx)
min = layer.minimumValue(idx)
interval = (max - min)/(number_classes -1 )

#classes
sum = min
classes = []

for i in range(number_classes):
    tmp = int(round(sum, 0))
    classes.append(tmp)
    sum += interval

t_list = []
init = 0

for i in range(number_classes):
    t_list.append(init)
    init += 1./(number_classes-1)


#FIRST AND LAST COLORS FROM PREDEFINED RAMP
c1 = QColor(ramp.color(1).name())
c2 = QColor(ramp.color(-1).name())

## OR SPECIFY YOUR OWN START AND END COLORS:
#c1 = QColor('blue')
#c2 = QColor('yellow')
#c1 = QColor('#0000ff')
#c2 = QColor('#00ffff')


colors2 = list(Color(c1.name()).range_to(Color(c2.name()), number_classes))
colors3 = [ QColor(element.hex) 
            for element in colors2 ]
colors = [ linear_color_interpolation(c1, c2, t) for t in t_list ]
color_list = [ QgsColorRampShader.ColorRampItem(classes[i], colors3[i])  for i in range(number_classes) ]
# for col in color_list:
#     print (col.color.name())

lower = sorted(vals)[0]
lower = sorted(vals)[1] 
upper = sorted(vals)[-1]
step = (upper-lower)/len(colors)
grades = []

for c in colors:
    cat = [lower, lower+step, c]
    lower = (lower+step)+0.1
    grades.append(cat)
range_list = []

for i in grades:
    sym = QgsSymbol.defaultSymbol(layer.geometryType())
    sym.setColor(QColor(i[2]))
    rng = QgsRendererRange(i[0], i[1], sym, '{0:.1f}-{1:.1f}'.format(i[0], i[1]))
    range_list.append(rng)

renderer = QgsGraduatedSymbolRenderer(fld, range_list)

layer.setRenderer(renderer)
layer.triggerRepaint()

print ('\nCOLOR RAMP: %s' %(str(colorRampNumber)))
print ('COLOR 1: %s COLOR 2: %s' %(ramp.color(1).name(),ramp.color(-1).name()))
print ('MIN: %s MAX:%s' %(str(min), str(max)))
print ('NUMBER OF CLASSES: %s'%(str(number_classes)))
print('*** ALL DONE :D ***')
1

You need to set the ramp to your layer and then run "updateClasses" - as an example:

renderer.setSourceColorRamp(ramp)
renderer.updateClasses(LAYER,QgsGraduatedSymbolRenderer.EqualInterval,NUM))
1

This has worked for me:

myRenderer.updateColorRamp(ramp)

It will update the colors acording to ramp without changing the breaks.

Link to docs: https://qgis.org/pyqgis/3.14/core/QgsGraduatedSymbolRenderer.html?highlight=equalinterval#qgis.core.QgsGraduatedSymbolRenderer.updateColorRamp

0

The accepted answer works, but I was looking for a method that could apply colour ramps to both raster and vector data.

Notes on previous answers:

  • The renderer.updateClasses method is deprecated since v3.10 (the QgsClassificationMethod class should be used instead).
  • QGIS is capable of interpolating colours for classes without using external libraries (but it't not very clear in the docs!).

I will post the code that worked for me below, for future reference / anyone searching for answers (using a red_white_blue_inverted colour ramp):

# Manually define the colour ramp - alternatively load from an XML (untested)
colour_list = [
    QColor(5, 113, 176),
    QColor(247, 247, 247),
    QColor(202, 0, 32),
]
colour_ramp_gradient.setColor1(QColor(5,113,176))
colour_ramp_gradient.setColor2(QColor(202,0,32))
colour_ramp_gradient.setStops(
   [QgsGradientStop(0.25, QColor(146,197,222)),
   QgsGradientStop(0.5, QColor(247,247,247)),
   QgsGradientStop(0.75, QColor(244,165,130))]
)

ramp_min = -20
ramp_max = 20
ramp_num_steps = 16

# Apply colour ramp for vector / points layer
if input_layer.type() == QgsMapLayer.VectorLayer:
    intervals = QgsClassificationEqualInterval().classes(
        ramp_min, ramp_max, ramp_num_steps)
    render_range_list = [QgsRendererRange(
        i, QgsSymbol.defaultSymbol(input_layer.geometryType()))
        for i in intervals]
    renderer = QgsGraduatedSymbolRenderer(
        values_field_name, render_range_list)
    renderer.updateColorRamp(colour_ramp_gradient)
    input_layer.setRenderer(renderer)

# Apply colour ramp for raster layer
elif input_layer.type() == QgsMapLayer.RasterLayer:
    num_intervals = len(colour_list)
    c_bounds = numpy.linspace(
        ramp_min, ramp_max, num_intervals).tolist()
    shader_interval_list = [QgsColorRampShader.ColorRampItem(
        c_bounds[i], colour_list[i], f'{c_bounds[i]}')
        for i in range(num_intervals)]
    cramp = QgsColorRampShader()
    cramp.setColorRampType(QgsColorRampShader.Interpolated)
    cramp.setColorRampItemList(shader_interval_list)
    shader = QgsRasterShader()
    shader.setRasterShaderFunction(cramp)
    input_layer.dataProvider().setNoDataValue(1, 0)
    renderer = QgsSingleBandPseudoColorRenderer(input_layer.dataProvider(), 1, shader)
    renderer.setOpacity(0.6)
    input_layer.setRenderer(renderer)

Your Answer

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

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