Select to view content in your preferred language

Troubles setting Consistent Symbology

190
4
02-05-2025 02:10 PM
DougBrowning
MVP Esteemed Contributor

I thought I had this figured out but it came back again.

In Pro if you try to set symbology outside the layer values it gets mad.  This is actually highly annoying because then we can never set consistent colors.

In code it seems to let me do it unless the scale is really small.  Like -0.0003 to 0.0003 the classify values below with fail.  Again highly annoying.  

So here I try to set 10 breaks.  If that fails I default to generic symbols.  Again I really do not want to do that as then the user cannot compare anything.

        sym = rasterLayer.symbology
        sym.updateColorizer('RasterClassifyColorizer')
        # was Condition Number
        sym.colorizer.colorRamp = project.listColorRamps('Prediction')[0]
        sym.colorizer.classificationField = "Value"
        sym.colorizer.breakCount = 10
        #sym.colorizer.noDataColor = {'RGB': [255, 255, 255, 100]}

        # manual breaks
        # this will fail on small time periods
        if len(sym.colorizer.classBreaks) == 10:
            sym.colorizer.classBreaks[0].upperBound = -100
            sym.colorizer.classBreaks[0].label = "Over -40%"
            sym.colorizer.classBreaks[1].upperBound = -40
            sym.colorizer.classBreaks[1].label = "-30% to -40%"
            sym.colorizer.classBreaks[2].upperBound = -30
            sym.colorizer.classBreaks[2].label = "-20% to -30%"
            sym.colorizer.classBreaks[3].upperBound = -20
            sym.colorizer.classBreaks[3].label = "-10% to -20%"
            sym.colorizer.classBreaks[4].upperBound = -10
            sym.colorizer.classBreaks[4].label = "0% to -10%"
            sym.colorizer.classBreaks[5].upperBound = 10
            sym.colorizer.classBreaks[5].label = "0% to 10%"
            sym.colorizer.classBreaks[6].upperBound = 20
            sym.colorizer.classBreaks[6].label = "10% to 20%"
            sym.colorizer.classBreaks[7].upperBound = 30
            sym.colorizer.classBreaks[7].label = "20% to 30%"
            sym.colorizer.classBreaks[8].upperBound = 40
            sym.colorizer.classBreaks[8].label = "30% to 40%"
            sym.colorizer.classBreaks[9].upperBound = 100
            sym.colorizer.classBreaks[9].label = "Over 40%"
        else:
            arcpy.AddMessage("Warning Trend Rasters may not work with short time frames. Trying reduced symbols.")
            sym.updateColorizer('RasterStretchColorizer')
            sym.colorizer.colorRamp = project.listColorRamps('Condition Number')[0]
            sym.colorizer.stretchType = "StandardDeviation"
        rasterLayer.name = "Total Change"
        rasterLayer.symbology = sym

 

But today we had a case where the value range was -7 to 8 and the breaks did set to -40 to 40 like I want BUT the entire map all draws at a single color.  So my check to look for not 10 breaks fix fails.  If I ask it how many breaks it does say 10.  And my bounds are correct.  Worse it sets the color to the first blue - so now the user thinks its all in the >-40% category.

DougBrowning_0-1738793246629.png

 

Any ideas on how to fix this?  I am pretty confused why symbology will not let me set anything I want.  We cannot have all the maps using a different color scale.  That is weird.

thanks

0 Kudos
4 Replies
HaydenWelch
MVP Regular Contributor

Couldn't quite get all the way there, but I was able to consistently get the label classes to generate with this:

I forgot to re-apply raster_sym to raster_layer.symbology. This actually works now

 

Note: The docstring for build_breaks is outdated, originally I was skipping 0 like in your example, but it seems to add the 0 break to the end if you do that, so now you can pass a 'zero_break' argument to change the label for 0 (default is 'No Change')

 

import arcpy
from arcpy._mp import ArcGISProject, Layer, ColorRamp, Symbology
from arcpy._symbology import RasterClassifyColorizer

from arcpy.cim import (
    CIMRasterLayer, 
    CIMRasterClassifyColorizer, 
    CIMRasterClassBreak, 
    ClassificationMethod, 
)

def build_breaks(min_break: int, max_break: int, total_breaks: int, 
                 units: str = '%', 
                 zero_break: str = 'No Change') -> list[tuple[int, str]]:
    """Build breaks and labels for the colorizer
    
    Args:
        min_break (int): The minimum break value (> -100)
        max_break (int): The maximum break value (< 100)
        total_breaks (int): The total number of breaks
        skip_zero (bool, optional): Skip the zero break if it is in the step. Defaults to True.
    """
    # Raise some errors if the input is invalid
    if min_break > max_break:
        raise ValueError("min_break must be less than max_break")
    
    if min_break <= -100:
        raise ValueError("min_break must be greater than -100")

    if max_break >= 100:
        raise ValueError("max_break must be less than 100")
    
    # Calculate the step (total_breaks has the 2 extremes removed)
    step = int((max_break - min_break) // (total_breaks-2))
    breaks = []
    
    # Add minimum extreme
    breaks.append((-100, f"Under {min_break}{units}"))
    
    # Build the breaks
    for upper_bound in range(min_break, max_break, step):
        if upper_bound == 0:
            breaks.append((upper_bound, zero_break))
            continue
        breaks.append((upper_bound, f"{upper_bound+step}{units} to {upper_bound}{units}"))
        
    # Add maximum extreme
    breaks.append((100, f"Over {max_break}{units}"))
    
    return breaks


def main():
    project: ArcGISProject = ArcGISProject(r"<path_to_aprx>")
    raster_layer: Layer = project.listMaps()[0].listLayers('<layer_name>')[0]
    
    layer_cim: CIMRasterLayer = raster_layer.getDefinition('V3')
    
    # Build the class breaks
    class_breaks = [CIMRasterClassBreak() for _ in range(10)]
    for (val, label), class_break in zip(build_breaks(-40, 40, 10), class_breaks):
        class_break: CIMRasterClassBreak = class_break
        class_break.label = label
        class_break.upperBound = val
    
    # Build the colorizer
    colorizer = CIMRasterClassifyColorizer()
    colorizer.classificationMethod = ClassificationMethod.Manual
    colorizer.classBreaks = class_breaks
    colorizer.field = "Value"
    
    layer_cim.colorizer.__dict__.update(colorizer.__dict__)
    raster_layer.setDefinition(layer_cim)
    
    raster_sym: Symbology = raster_layer.symbology
    sym_colorizer: RasterClassifyColorizer = raster_sym.colorizer
    color_ramp: ColorRamp = project.listColorRamps('Prediction')[0]
    sym_colorizer.colorRamp = color_ramp
    
    raster_layer.symbology = raster_sym
    
    # Save the project
    project.save()

 

 

0 Kudos
DougBrowning
MVP Esteemed Contributor

Sorry but I do not get your post.  I need the breaks to be exact not based on the data range at all.  All rasters need to be exactly the same symbology.

This is getting a lot of attention at the office and I am still stuck.  For now I test to see if the range is less than 20 and I set it to a generic scale.  Not sure why it ignores it or how to tell it did ignore it.

thanks

0 Kudos
DougBrowning
MVP Esteemed Contributor

I added a check for small ranges but now that is also failing.

arcpy.management.GetRasterProperties(rasterLayer, "MAXIMUM")  will fail if the range is really small but works just fine if the range is larger.

Something is up here.

0 Kudos
JeffBarrette
Esri Regular Contributor

@DougBrowning I've taken a look at this and I'm also bringing this to the attention of the raster team.  A few things. 

1) With Python, I'm able to standardize symbology across multiple rasters BUT only if I set the lowerBound of the raster.  This property was made available on GraduatedColorsRenderer, GraduatedSymbolsRenderer and RasterClassifyColorizer at Pro 3.4.

 

#Standardize multiple RasterClassifyColorizer elevation datasets

p = arcpy.mp.ArcGISProject('current')

m = p.listMaps('Yosemite Elevation')[0]

for l in m.listLayers('*dem'):

    if l.isRasterLayer:

        lBound = 0

        uBound = 300

        sym = l.symbology

        sym.colorizer.colorRamp = p.listColorRamps('Elevation #1')[0]

        sym.colorizer.breakCount = 10 #final range: 0-3000

        sym.colorizer.lowerBound = 0

        for brk in sym.colorizer.classBreaks:

            brk.upperBound = uBound

            brk.label = f"{lBound} - {uBound} meters"

            lBound += 300

            uBound += 300

        l.symbology = sym

2) It appears I can't do this in the UI.  The UI does not let me create ranges below the minimum value.  It lets me create ranges for upper values that don't exist with no issue.  If I try to set the lower bound via the Histogram tab but changing it to Zero has no effect on the colorizer (Unlike the GraduatedColors/symbols renderers). Even if I successfully set symbology via arcpy.mp like above, when I view the symbology pane, I get a warning.

3) There is a bug you may run into concerning labels.  During Pro 3.4 development, auto apply labels behavior was added to the UI behavior and it causes arcpy.mp class label modifications to get reset.  We have a fix for this at Pro 3.5 and hope to port it to a patch for 3.4.  The bug is BUG-000174482.

I hope this helps,

Jeff - arcpy.mp and Layout (SDK) teams