I just wanted to reach out and say that I think this is a valuable tool. I was able to use this script and with a couple of modifications, it will change the pop-up title for each layer to be the same as the layer name for all layers in a web map. I will paste the script below with a description of the changes my colleague and I made in case anyone else is interested!
The following changes are marked by comments in the code.
The only part a user would need to update is the WEBMAP_ID variable to identify the web map they are working with. This can be run in the Notebook tab of your ArcGIS Online or Enterprise Portal.
from arcgis.gis import GIS
import json
import copy
gis = GIS("home")
WEBMAP_ID = "ID for layername"
expr_name = "PointType2"
expr_title = "Point Type 2"
expr_code = """
var info = GetFeatureSetInfo(GetFeatureSet($feature));
return info.webMapLayerTitle;
""".strip()
item = gis.content.get(WEBMAP_ID)
data = item.get_data()
print([l.get("title") for lyr in data["operationalLayers"] for l in lyr.get("layers", [lyr])]) # Change 3
for lyr in data.get("operationalLayers", []):
layers_to_process = lyr.get("layers", [lyr])
for l in layers_to_process: # Change 1
popup = l.setdefault("popupInfo", {})
expressions = popup.setdefault("expressionInfos", [])
# Avoid duplicate expression
existing = next((e for e in expressions if e.get("name") == expr_name), None)
if existing:
existing["title"] = expr_title
existing["expression"] = expr_code
else:
expressions.append({
"name": expr_name,
"title": expr_title,
"expression": expr_code
})
popup["title"] = l.get("title") or lyr.get("title") or "" # Change 2
# Add to description without overwriting existing popup content
desc = popup.get("description", "")
layer_line = f"Layer: {{expression/{expr_name}}}<br>"
if f"{{expression/{expr_name}}}" not in desc:
popup["description"] = layer_line + desc
item.update(item_properties={
"text": json.dumps(data)})
print("Updated popups for all operational layers.")
Or something like this if you want to keep all other fields in popup:
# --- Web map popups: Title = Layer Name, Body = All Fields table (like your screenshot) ---from arcgis.gis import GISfrom arcgis.features import FeatureLayerimport json, datetime
WEBMAP_ID = "yourmapid" # your mapDRY_RUN = False # True = preview, don't saveINCLUDE_SYSTEM_FIELDS = True # keep OBJECTID/SHAPE_*/editor tracking, etc.
# Arcade expression: prefer the web map's layer title; fallback to service layer nameARC_EXPR = """var info = GetFeatureSetInfo($layer);var t = info.webMapLayerTitle;var n = info.layerName;return IIf(IsEmpty(t), IIf(IsEmpty(n), 'Layer', n), t);"""
def _expr_infos():return [{"name": "expr0","title": "Layer Name","expression": ARC_EXPR,"returnType": "string"}]
def _get_fields_from(layer_dict, gis):"""Return list of {'fieldName','label','visible'} in the service order."""# 1) Try popupInfo.fieldInfos first (to preserve existing aliases/order if present)pi = layer_dict.get("popupInfo")if isinstance(pi, dict):finfos = pi.get("fieldInfos")if isinstance(finfos, list) and finfos:out = []for f in finfos:if not isinstance(f, dict):continuename = f.get("fieldName") or f.get("name")if not name:continueout.append({"fieldName": name, "label": f.get("label", name), "visible": True})if out:return out
# 2) layerDefinition.fields (common for map-image sublayers / feature collections)ld = layer_dict.get("layerDefinition")if isinstance(ld, dict) and isinstance(ld.get("fields"), list) and ld["fields"]:out = []for f in ld["fields"]:nm = f.get("name")if not nm:continueout.append({"fieldName": nm, "label": f.get("alias", nm), "visible": True})if out:return out
# 3) Service fields via URLurl = layer_dict.get("url")if url:try:fl = FeatureLayer(url, gis=gis)fields = getattr(fl.properties, "fields", None)out = []if isinstance(fields, list):for f in fields:nm = f.get("name")if not nm:continueout.append({"fieldName": nm, "label": f.get("alias", nm), "visible": True})if out:return outexcept Exception:pass
# 4) FeatureCollection inner layerDefinitionfc = layer_dict.get("featureCollection")if isinstance(fc, dict):for fcl in (fc.get("layers") or []):fld = fcl.get("layerDefinition")if isinstance(fld, dict) and isinstance(fld.get("fields"), list) and fld["fields"]:out = []for f in fld["fields"]:nm = f.get("name")if not nm:continueout.append({"fieldName": nm, "label": f.get("alias", nm), "visible": True})if out:return out
return []
def _maybe_filter_system(fields):if INCLUDE_SYSTEM_FIELDS:return fieldssys_prefixes = ("SHAPE",)exclude_exact = {"OBJECTID","FID","GLOBALID","GLOBAL_ID","CreationDate","Creator","EditDate","Editor","created_date","created_user","last_edited_date","last_edited_user"}out = []for f in fields:nm = f["fieldName"]up = nm.upper()if up in exclude_exact or any(up.startswith(p) for p in sys_prefixes):continueout.append(f)return out
def _ensure_popup(layer_dict, gis, path, changes):"""Replace popup with: title expr + fields table; applies to any schema location."""changed = False
# enable popups if disabledif layer_dict.get("disablePopup") is True:layer_dict["disablePopup"] = Falsechanged = Truechanges.append(f"{path}: enabled popups")
# locations where popupInfo may livespots = []
# directif isinstance(layer_dict.get("popupInfo"), dict):spots.append(("popupInfo", layer_dict["popupInfo"]))else:layer_dict["popupInfo"] = {}spots.append(("popupInfo(created)", layer_dict["popupInfo"]))changed = True
# map-image sublayerld = layer_dict.get("layerDefinition")if isinstance(ld, dict):if isinstance(ld.get("popupInfo"), dict):spots.append(("layerDefinition.popupInfo", ld["popupInfo"]))else:ld["popupInfo"] = {}spots.append(("layerDefinition.popupInfo(created)", ld["popupInfo"]))layer_dict["layerDefinition"] = ldchanged = True
# feature collection inner layersfc = layer_dict.get("featureCollection")if isinstance(fc, dict):for idx, fcl in enumerate(fc.get("layers") or []):if isinstance(fcl.get("popupInfo"), dict):spots.append((f"featureCollection.layers[{idx}].popupInfo", fcl["popupInfo"]))else:fcl["popupInfo"] = {}spots.append((f"featureCollection.layers[{idx}].popupInfo(created)", fcl["popupInfo"]))changed = True
# unified fields list (service order + aliases)fields = _get_fields_from(layer_dict, gis)fields = _maybe_filter_system(fields)
for label, pi in spots:# Title via Arcadepi["title"] = "{expression/expr0}"pi["expressionInfos"] = _expr_infos()
# Replace body with a single fields table exactly like the viewer's tablepi["popupElements"] = [{"type": "fields", "fieldInfos": fields}]
# For older viewers, keep fieldInfos mirrored too (order/labels)pi["fieldInfos"] = fields
# Clean extras so only table shows (like your screenshot)pi["description"] = Nonepi["mediaInfos"] = []pi["showAttachments"] = False
changed = True
if changed:changes.append(f"{path}: enforced title+fields-table ({len(fields)} fields)")return changed
def _walk(node, gis, path, changes):changed = Falseif isinstance(node, dict):if _ensure_popup(node, gis, path, changes):changed = True# Recurse childrenfor i, child in enumerate(node.get("layers") or []):if isinstance(child, dict):if _walk(child, gis, f"{path}.layers[{i}]", changes):changed = Truefor i, child in enumerate(node.get("tables") or []):if isinstance(child, dict):if _walk(child, gis, f"{path}.tables[{i}]", changes):changed = Truefc = node.get("featureCollection")if isinstance(fc, dict):for i, fcl in enumerate(fc.get("layers") or []):if isinstance(fcl, dict):if _walk(fcl, gis, f"{path}.featureCollection.layers[{i}]", changes):changed = Truereturn changed
def run(webmap_id, dry_run=False):gis = GIS("home")item = gis.content.get(webmap_id)if not item:raise RuntimeError("Web map not found")data = item.get_data()if not data:raise RuntimeError("No web map data")
print(f"Loaded: {item.title} ({item.id})")changes = []changed_any = False
for i, lyr in enumerate(data.get("operationalLayers", []) or []):if _walk(lyr, gis, f"operationalLayers[{i}]", changes):changed_any = Truefor i, tbl in enumerate(data.get("tables", []) or []):if _walk(tbl, gis, f"tables[{i}]", changes):changed_any = True
if not changed_any:print("No changes necessary.")return {"updated": False, "changes": changes}
backup = f"webmap_backup_{item.id}_{datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')}.json"with open(backup, "w", encoding="utf-8") as f:json.dump(data, f, indent=2)print(f"Backup saved: {backup}")
if dry_run:print("\nDRY RUN — sample changes:")for line in changes[:30]:print(" •", line)if len(changes) > 30:print(f"... plus {len(changes)-30} more")return {"updated": False, "changes": changes, "backup": backup}
ok = item.update(data=data)if not ok:raise RuntimeError("Item update returned False")
print("\n✅ Updated. Open the **web map** (not an app) in a NEW tab and hard-refresh (Ctrl+F5).")for line in changes[:30]:print(" •", line)if len(changes) > 30:print(f"... plus {len(changes)-30} more")return {"updated": True, "changes": changes, "backup": backup}
result = run(WEBMAP_ID, DRY_RUN)result
If you want to do this for all the layers in the map you can use notebooks to automate it:
from arcgis.gis import GISimport jsonimport copy
WEBMAP_ID = "PASTE_WEBMAP_ITEM_ID_HERE"
expr_name = "expr_layer_name"expr_title = "Layer Name"expr_code = """var info = GetFeatureSetInfo(GetFeatureSet($feature));return info.webMapLayerTitle;""".strip()
item = gis.content.get(WEBMAP_ID)data = item.get_data()
for lyr in data.get("operationalLayers", []):# Skip layers without popups if desiredpopup = lyr.setdefault("popupInfo", {})
# Add expressionInfos if missingexpressions = popup.setdefault("expressionInfos", [])
# Avoid duplicate expressionexisting = next((e for e in expressions if e.get("name") == expr_name), None)
if existing:existing["title"] = expr_titleexisting["expression"] = expr_codeelse:expressions.append({"name": expr_name,"title": expr_title,"expression": expr_code})
# Add to description without overwriting existing popup contentdesc = popup.get("description", "")
if f"{{expression/{expr_name}}}" not in desc:popup["description"] = layer_line + desc
item.update(item_properties={"text": json.dumps(data)})
Please provide some snips on how you are trying to use this expression.
Thanks for this! The expression isn't working in my Arcade item in the popup. Did you envision it elsewhere?
We found a workaround so you dont have to type this in for every layer: return GetFeatureSetInfo($layer).webMapLayerTitle is the expression that returns the layer name in the popup.
The default was that layer name was displayed unless you change it to be driven by the attribute. How do we set up expression to show layer name without typing this in manually for every layer?
Signed in members can post, follow updates, and more. New here? Register a free account.
Find useful guides, FAQs, and documents to help you navigate and make the most of Esri Community.