Hi,
I am a GIS Analyst/Urban Planner working with land use data. I have a standard land use symbology for use in different cities and counties.
I am trying to extract the hex codes from the layer symbology and add them as a new field. The script from this solution post by @JohannesLindner . The script works to add the label of my symbology layer to a new field:
import arcpy
layer_names = ["PW Zoning"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: label}
item_dict = {tuple(i.values[0]): i.label for i in items}
# add a new field
arcpy.management.AddField(layer, "PW_Zoning_Designation", "TEXT")
# insert the label values
with arcpy.da.UpdateCursor(layer, ["PW_Zoning_Designation"] + fields) as cursor:
for row in cursor:
key = tuple([str(x) for x in row[1:]]) # item.values stores values as string
try:
row[0] = item_dict[key]
cursor.updateRow(row)
except KeyError: # no symbology found
pass
I am curious how I would add the hex codes into this code to add a new field that would include the six-digit hex codes; RGB codes work too. Thanks for any help. I am new to arcpy and will be attending the Developer Conference in two months.
Solved! Go to Solution.
The original script was for Unique Values from multiple fields. You have Unique Values from one field in groups. These are represented differently in Python.
This should work:
layer_names = ["TestPolygons"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: label}
item_dict = dict()
for item in items:
for v in item.values:
item_dict[v[0]] = item.label
# add a new field
arcpy.management.AddField(layer, "LandUseDesignation1", "TEXT")
# insert the label values
with arcpy.da.UpdateCursor(layer, ["LandUseDesignation1"] + fields) as cursor:
for row in cursor:
key = row[-1]
try:
row[0] = item_dict[key]
cursor.updateRow(row)
except KeyError: # no symbology found
pass
We have to change how we build the item_dict (lines 12-15) and how to get values from it (line 21).
As it turns out, getting a symbol's color is easy, but there's no built-in way to get the hex value...
If you have defined all your symbol colors in the RGB system, you're good to go:
layer_names = ["TestPolygons"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: iteml}
item_dict = {tuple(item.values[0]): item for item in items}
# add a new field
arcpy.management.AddField(layer, "PW_Zoning_Designation", "TEXT")
arcpy.management.AddField(layer, "ColorRaw", "TEXT")
arcpy.management.AddField(layer, "ColorHex", "TEXT")
# insert the label and color values
with arcpy.da.UpdateCursor(layer, ["PW_Zoning_Designation", "ColorRaw", "ColorHex"] + fields) as cursor:
for row in cursor:
# get the item
key = tuple([str(x) for x in row[3:]]) # item.values stores values as string
try:
item = item_dict[key]
# no item found -> default value and skip to the next feature
except KeyError:
row[0] = "No Symbol"
cursor.updateRow(row)
continue
# store the item's label
row[0] = item.label
# get the symbol color
color = item.symbol.color
color_type = list(color.keys())[0]
# if it's rgb, convert to hex
if color_type == "RGB":
rgb = color["RGB"][:3]
row[2] = '#%02x%02x%02x' % tuple(rgb)
# if it's something else (hsv, hsl, cmyk), you have to convert that in other ways... here I just don't convert
else:
row[2] = None
# store the color value as string for documentation
row[1] = str(color)
# write values
cursor.updateRow(row)
Note that we're storing the whole symbology item in the dictionary (line 12), not only the label. This way, we can access the item's symbol color in the for-loop (line 33).
Colors can be defined in different systems (RGB, HSV, HSL, CMYK, ...). Converting from RGB to hex is pretty straightforward (line 38). If you have other color systems, you probably have to convert those values into RGB first (line 41). A good start could be the module colorsys, which is part of the default Python installation. It might be easier to go through your symbology and just switch the system there.
whoa! thank you so much @JohannesLindner
one last quick question...when I run the initial code on Grouped Values, it only searches for the first value in the group and applies the new Label. See screenshot...how can I get the initial code to search through each land use code after the semi-colon and add the new label?
sample code again:
import arcpy
layer_names = ["ZoningPlacerCo"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: label}
item_dict = {tuple(i.values[0]): i.label for i in items}
# add a new field
arcpy.management.AddField(layer, "LandUseDesignation1", "TEXT")
# insert the label values
with arcpy.da.UpdateCursor(layer, ["LandUseDesignation1"] + fields) as cursor:
for row in cursor:
key = tuple([str(x) for x in row[1:]]) # item.values stores values as string
try:
row[0] = item_dict[key]
cursor.updateRow(row)
except KeyError: # no symbology found
pass
The original script was for Unique Values from multiple fields. You have Unique Values from one field in groups. These are represented differently in Python.
This should work:
layer_names = ["TestPolygons"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: label}
item_dict = dict()
for item in items:
for v in item.values:
item_dict[v[0]] = item.label
# add a new field
arcpy.management.AddField(layer, "LandUseDesignation1", "TEXT")
# insert the label values
with arcpy.da.UpdateCursor(layer, ["LandUseDesignation1"] + fields) as cursor:
for row in cursor:
key = row[-1]
try:
row[0] = item_dict[key]
cursor.updateRow(row)
except KeyError: # no symbology found
pass
We have to change how we build the item_dict (lines 12-15) and how to get values from it (line 21).
epic! thank you. I am familiar with geopandas and folium. arcpy is new to me. I appreciate your explanations and comments (#)
sorry, quick question @JohannesLindner . how would you combine adding the color code with multiple fields with this new code that searches for grouped values?
specifically updating the code below with the item_dict (lines 12-15) and line 21:
layer_names = ["TestPolygons"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: iteml}
item_dict = {tuple(item.values[0]): item for item in items}
# add a new field
arcpy.management.AddField(layer, "PW_Zoning_Designation", "TEXT")
arcpy.management.AddField(layer, "ColorRaw", "TEXT")
arcpy.management.AddField(layer, "ColorHex", "TEXT")
# insert the label and color values
with arcpy.da.UpdateCursor(layer, ["PW_Zoning_Designation", "ColorRaw", "ColorHex"] + fields) as cursor:
for row in cursor:
# get the item
key = tuple([str(x) for x in row[3:]]) # item.values stores values as string
try:
item = item_dict[key]
# no item found -> default value and skip to the next feature
except KeyError:
row[0] = "No Symbol"
cursor.updateRow(row)
continue
# store the item's label
row[0] = item.label
# get the symbol color
color = item.symbol.color
color_type = list(color.keys())[0]
# if it's rgb, convert to hex
if color_type == "RGB":
rgb = color["RGB"][:3]
row[2] = '#%02x%02x%02x' % tuple(rgb)
# if it's something else (hsv, hsl, cmyk), you have to convert that in other ways... here I just don't convert
else:
row[2] = None
# store the color value as string for documentation
row[1] = str(color)
# write values
cursor.updateRow(row)
here is my sample code trying to iterate through groups and apply color column value attributes. I am getting an index error saying 'IndexError: list index out of range'
layer_names = ["ZoningPlacerCo"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# get the symbology fields and items
renderer = layer.symbology.renderer
fields = renderer.fields
items = renderer.groups[0].items
# create a dict {values: iteml}
item_dict = dict()
for item in items:
for v in item.vlues:
item_dict[v[0]] = item.label
# add a new field
arcpy.management.AddField(layer, "PW_Zoning_Designation", "TEXT")
arcpy.management.AddField(layer, "ColorRaw", "TEXT")
arcpy.management.AddField(layer, "ColorHex", "TEXT")
# insert the label and color values
with arcpy.da.UpdateCursor(layer, ["PW_Zoning_Designation", "ColorRaw", "ColorHex"] + fields) as cursor:
for row in cursor:
# get the item
key = tuple([str(x) for x in row[3:]]) # item.values stores values as string
try:
item = item_dict[key]
# no item found -> default value and skip to the next feature
except KeyError:
row[0] = "No Symbol"
cursor.updateRow(row)
continue
# store the item's label
row[0] = item.label
# get the symbol color
color = item.symbol.color
color_type = list(color.keys())[0]
# if it's rgb, convert to hex
if color_type == "RGB":
rgb = color["RGB"][:3]
row[2] = '#%02x%02x%02x' % tuple(rgb)
# if it's something else (hsv, hsl, cmyk), you have to convert that in other ways... here I just don't convert
else:
row[2] = None
# store the color value as string for documentation
row[1] = str(color)
# write values
cursor.updateRow(row)
the error is on line ten: items = renderer.groups[0].items
I'm not quite sure what you mean by combine. Do you want to also get the colors from a symbology with grouped unique values? Or do you have layers with grouped values from multiple fields? Eh, let's do both...
To extract the item dict for different combinations of (multiple fields) and (grouped values), we have to generalize a bit. Let's take a look at how these combinations are represented in Python:
# 4 layers with different Unique Value symbologies
layer_names = [
"unique", # just a plain UV symbology
"unique_grouped", # UV from a single field, but in groups
"unique_multiple", # UV from multiple fields, ungrouped
"unique_multiple_grouped", # UV from multiple fields, grouped
]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
layer = aprx.activeMap.listLayers(name)[0]
renderer = layer.symbology.renderer
first_item = renderer.groups[0].items[0]
print(f"layer:\t{layer.name}\nfields:\t{renderer.fields}\nvalues:\t{first_item.values}\n\n")
layer: unique fields: ['TextField1'] values: [['A']] layer: unique_grouped fields: ['TextField1'] values: [['A'], ['B']] layer: unique_multiple fields: ['TextField1', 'IntegerField1'] values: [['A', '12345']] layer: unique_multiple_grouped fields: ['TextField1', 'IntegerField1'] values: [['A', '12345'], ['C', '2']]
values is a list of lists. There is an inner list for each value in the group. In these lists, there is a (string!) value for each field.
We want to use these lists as dictionary keys, but that's impossible, because keys have to be immutable, and lists are mutable, so we change them to immutable tuples. Also, we have to have a separate key for each value in a group.
def get_item_dict(symbology):
if symbology.renderer.type != "UniqueValueRenderer":
raise ValueError("symbology has to be Unique Value!")
item_dict = dict()
for group in symbology.renderer.groups:
for item in group.items:
for value_group in item.values:
item_dict[tuple(value_group)] = item
return item_dict
Also, let's put the color conversion into a separate function to make the code a bit cleaner...
def convert_to_hex(color):
color_type = list(color.keys())[0]
if color_type == "RGB":
rgb = color["RGB"][:3]
return "#%02x%02x%02x" % tuple(rgb)
else:
return None
And with these two functions, we can concentrate on the actual workflow:
layer_names = ["TestPolygons"]
aprx = arcpy.mp.ArcGISProject("current")
for name in layer_names:
# get the layer
layer = aprx.activeMap.listLayers(name)[0]
# create a dict {values: item}
item_dict = get_item_dict(layer.symbology)
# add a new field
arcpy.management.AddField(layer, "PW_Zoning_Designation", "TEXT")
arcpy.management.AddField(layer, "ColorRaw", "TEXT")
arcpy.management.AddField(layer, "ColorHex", "TEXT")
# insert the label and color values
with arcpy.da.UpdateCursor(layer, ["PW_Zoning_Designation", "ColorRaw", "ColorHex"] + layer.symbology.renderer.fields) as cursor:
for row in cursor:
# get the item
key = tuple([str(x) for x in row[3:]]) # item.values stores values as string
try:
item = item_dict[key]
# no item found -> default value and skip to the next feature
except KeyError:
row[0] = "No Symbol"
cursor.updateRow(row)
continue
# store the item's label
row[0] = item.label
# store the raw color
row[1] = str(item.symbol.color)
# store the hex value
row[2] = convert_to_hex(item.symbol.color)
# write values
cursor.updateRow(row)