Trying to find a way programmatically (arcpy) move the legend if it intercepts features within a data frame, in the scenario below, if the legend obscures the view of the AOI, then I want it to move to a different corner until its not an issue. This has to be on top of the data frame as opposed to making the data frame smaller and just putting it to the side.
-
1If you are using Data Driven Pages you might find some assistance in this: gis.stackexchange.com/questions/167975/…. More generally I'd search Google on something like "move legend in Data Driven Pages" for some more suggestions. With fixed legends I've converted them to an image and used the following to move them around: support.esri.com/en/technical-article/000011951 None of these are answers, just workarounds.– JohnCommented Sep 11, 2017 at 12:20
-
Yes im currently using data driven pages, thanks for the link Johns– SlevyCommented Sep 13, 2017 at 0:54
3 Answers
import arcpy, traceback, os, sys, time
from arcpy import env
import numpy as np
env.overwriteOutput = True
outFolder=arcpy.GetParameterAsText(0)
env.workspace = outFolder
dpi=2000
tempf=r'in_memory\many'
sj=r'in_memory\sj'
## ERROR HANDLING
def showPyMessage():
arcpy.AddMessage(str(time.ctime()) + " - " + message)
try:
mxd = arcpy.mapping.MapDocument("CURRENT")
allLayers=arcpy.mapping.ListLayers(mxd,"*")
ddp = mxd.dataDrivenPages
df = arcpy.mapping.ListDataFrames(mxd)[0]
SR = df.spatialReference
## GET LEGEND ELEMENT
legendElm = arcpy.mapping.ListLayoutElements(mxd, "LEGEND_ELEMENT", "myLegend")[0]
# GET PAGES INFO
thePagesLayer = arcpy.mapping.ListLayers(mxd,ddp.indexLayer.name)[0]
fld = ddp.pageNameField.name
# SHUFFLE THROUGH PAGES
for pageID in range(1, ddp.pageCount+1):
ddp.currentPageID = pageID
aPage=ddp.pageRow.getValue(fld)
arcpy.RefreshActiveView()
## DEFINE WIDTH OF legend IN MAP UNITS..
E=df.extent
xmin=df.elementPositionX;xmax=xmin+df.elementWidth
x=[xmin,xmax];y=[E.XMin,E.XMax]
aX,bX=np.polyfit(x, y, 1)
w=aX*legendElm.elementWidth
## and COMPUTE NUMBER OF ROWS FOR FISHNET
nRows=(E.XMax-E.XMin)//w
## DEFINE HEIGHT OF legend IN MAP UNITS
ymin=df.elementPositionY;ymax=ymin+df.elementHeight
x=[ymin,ymax];y=[E.YMin,E.YMax]
aY,bY=np.polyfit(x, y, 1)
h=aY*legendElm.elementHeight
## and COMPUTE NUMBER OF COLUMNS FOR FISHNET
nCols=(E.YMax-E.YMin)//h
## CREATE FISHNET WITH SLIGHTLY BIGGER CELLS (due to different aspect ratio between legend and dataframe)
origPoint='%s %s' %(E.XMin,E.YMin)
yPoint='%s %s' %(E.XMin,E.YMax)
endPoint='%s %s' %(E.XMax,E.YMax)
arcpy.CreateFishnet_management(tempf, origPoint,yPoint,
"0", "0", nCols, nRows,endPoint,
"NO_LABELS", "", "POLYGON")
arcpy.DefineProjection_management(tempf, SR)
## CHECK CORNER CELLS ONLY
arcpy.SpatialJoin_analysis(tempf, tempf, sj, "JOIN_ONE_TO_ONE",
match_option="SHARE_A_LINE_SEGMENT_WITH")
nCorners=0
with arcpy.da.SearchCursor(sj, ("Shape@","Join_Count")) as cursor:
for shp, neighbours in cursor:
if neighbours!=3:continue
nCorners+=1; N=0
for lyr in allLayers:
if not lyr.visible:continue
if lyr.isGroupLayer:continue
if not lyr.isFeatureLayer:continue
## CHECK IF THERE ARE FEATURES INSIDE CORNER CELL
arcpy.Clip_analysis(lyr, shp, tempf)
result=arcpy.GetCount_management(tempf)
n=int(result.getOutput(0))
N+=n
if n>0: break
## IF NONE, CELL FOUND; COMPUTE PAGE COORDINATES FOR LEGEND AND BREAK
if N==0:
tempRaster=outFolder+os.sep+aPage+".png"
e=shp.extent;X=e.XMin;Y=e.YMin
x=(X-bX)/aX;y=(Y-bY)/aY
break
if nCorners==0: N=1
## IF NO CELL FOUND PLACE LEGEND OUTSIDE DATAFRAME
if N>0:
x=df.elementPositionX+df.elementWidth
y=df.elementPositionY
legendElm.elementPositionY=y
legendElm.elementPositionX=x
outFile=outFolder+os.sep+aPage+".png"
arcpy.AddMessage(outFile)
arcpy.mapping.ExportToPNG(mxd,outFile)
except:
message = "\n*** PYTHON ERRORS *** "; showPyMessage()
message = "Python Traceback Info: " + traceback.format_tb(sys.exc_info()[2])[0]; showPyMessage()
message = "Python Error Info: " + str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"; showPyMessage()
NOTES: For each page in data driven pages script attempts to find enough room in dataframe corners to place Legend (called myLegend) without covering any visible feature layer. Script uses fishnet to identify corner cells. Cell dimension is slightly greater than Legend dimension in data view units. Corner cell is the one that shares a boundary with 3 neighbours. If no corners or room found, Legend placed outside dataframe on layout page.
Unfortunately I don't know how manage page definition query. Points shown were originally scattered all around RECTANGLE extent, with some of them having no association with pages. Arcpy still sees entire layer, although I applied definition query (match) to the points.
-
Thanks for the great write up on this Felix, though im having issues implementing this solution to work as fluid as your example, as detailed as this is, is there anything I should be aware of when creating the map document, legend anchor points, etc?– SlevyCommented Sep 18, 2017 at 6:09
-
1Anchor points are lower left for both legend and data frame. How did I forget this?– FelixIPCommented Sep 18, 2017 at 6:11
-
Yep, definitely made a difference in test here. If I wanted to switch the anchor point to the middle (for the dataframe) i'm assuming the entire logic is out of whack? which part would I need to configure. Just lines 33 to 44?– SlevyCommented Sep 18, 2017 at 6:39
-
1Compute xmin and xmax through width and position x. Similar with y axis. Not sure why do you need it...– FelixIPCommented Sep 18, 2017 at 7:01
-
Part of another workflow, thanks Felix, great step forward here– SlevyCommented Sep 18, 2017 at 7:14
The way that I would do this would be to create a "legend element" feature class that represents your legend element in the same coordinate system as those features.
That way you could use Select Layer By Location to test whether your legend element overlaps with any features, and move it if it does.
Its non-trivial but eminently doable and there is a Q&A on this site (Convert point XY to page units XY using arcpy?) that could be used to work out the hardest part of converting between page and map coordinates.
-
1The hardest part is findinga gap big enough to fit legend box.– FelixIPCommented Sep 11, 2017 at 9:31
-
1@FelixIP Why so? It sounds like the asker is restricting themselves to just testing four corners of the data frame. I assume they have a rule for what happens if no corner is suitable.– PolyGeo ♦Commented Sep 11, 2017 at 11:07
-
I think this is way to go, though the gap in the legend will probably be the least of my problems. Ideally, the scale of the map will continue to change until the legend doesn't intercept the polygon of interest. Though would like to hear or see some practical examples people have attempted!– SlevyCommented Sep 13, 2017 at 0:53
Below is code I've used to move legends and inset maps so as not to obscure data. You asked about the check intersect function on another thread. This is my implementation of someone else's code. I don't recall exactly where it's from. It was a script to move an inset map for a state in New England I think.
inset is the handle for the legend or inset map element.
#check intersect function
def checkIntersect(MovableObject):
#get absolute x and y disatnce of MovableObject in page units
PageOriginDistX = (inset.elementPositionX + inset.elementWidth) - DataFrame.elementPositionX #Xmax in page units
PageOriginDistY = (inset.elementPositionY + inset.elementHeight) - DataFrame.elementPositionY #absolute y disatnce of element
#Generate x/y pairs for new tempfile used to test intersection of original MovableObject placement
Xmax = DataFrame.extent.XMin + ((DataFrame.extent.XMax - DataFrame.extent.XMin) *
(PageOriginDistX / DataFrame.elementWidth))
Xmin = DataFrame.extent.XMin + ((DataFrame.extent.XMax - DataFrame.extent.XMin) *
((inset.elementPositionX - DataFrame.elementPositionX) / DataFrame.elementWidth))
Ymax = DataFrame.extent.YMin + ((DataFrame.extent.YMax - DataFrame.extent.YMin) *
(PageOriginDistY / DataFrame.elementHeight))
Ymin = DataFrame.extent.YMin + ((DataFrame.extent.YMax - DataFrame.extent.YMin) *
((inset.elementPositionY - DataFrame.elementPositionY) / DataFrame.elementHeight))
#list of coords for temp polygon
coordList = [[[Xmax,Ymax], [Xmax,Ymin], [Xmin,Ymin], [Xmin,Ymax]]]
#create empty temp poly as tempShape, give it a spatial ref, make it into a featureclass so it works
#with intersect
tempShape = os.path.join(sys.path[0], "temp.shp")
arcpy.CreateFeatureclass_management(sys.path[0], "temp.shp","POLYGON")
array = arcpy.Array()
point = arcpy.Point()
featureList = []
arcpy.env.overwriteOutput = True
for feature in coordList:
for coordPair in feature:
point.X = coordPair[0]
point.Y = coordPair[1]
array.add(point)
array.add(array.getObject(0))
polygon = arcpy.Polygon(array)
array.removeAll()
featureList.append(polygon)
arcpy.CopyFeatures_management(featureList, tempShape)
arcpy.MakeFeatureLayer_management(tempShape, "tempShape_lyr")
#check for intersect
arcpy.SelectLayerByLocation_management("unobscured_lyr", "INTERSECT", "tempShape_lyr", "", "NEW_SELECTION")
#initiate search and count
polyCursor = arcpy.SearchCursor("unobscured_lyr")
polyRow = polyCursor.next()
count = 0
#Clear Selection
arcpy.SelectLayerByAttribute_management("unobscured_lyr","CLEAR_SELECTION")
#Delete the temporary shapefile.
arcpy.Delete_management(tempShape)
#count
while polyRow:
count = count + 1
polyRow = polyCursor.next()
#Clear Selection
arcpy.SelectLayerByAttribute_management("unobscured_lyr","CLEAR_SELECTION")
#Delete the temporary shapefile.
arcpy.Delete_management(tempShape)
#Return the count value to main part of script to determine placement of locator map.
return count
Then, the code below from this post (Data Driven Pages with Movable Legend/Inset Map) should make more sense.
for pageNum in range(1, mxd.dataDrivenPages.pageCount + 1):
#setup naming and path for output maps
path = mxd.filePath
bn = os.path.basename(path)[:-4]
mxd.dataDrivenPages.currentPageID = pageNum
insetDefaultX = inset.elementPositionX
insetDefaultY = inset.elementPositionY
#check defualt position for intersect
intersect = checkIntersect(inset)
if intersect == 0: #if it doesn't intersect, print the map
arcpy.mapping.ExportToEPS(mxd, exportFolder + "\\" + bn + "_"+ str(pageNum) + ".eps", "Page_Layout",640,480,300,"BETTER","RGB",3,"ADAPTIVE","RASTERIZE_BITMAP",True,False)
else: #intersect != 0: #move inset to SE corner
inset.elementPositionX = (DataFrame.elementPositionX + DataFrame.elementWidth) - inset.elementWidth
inset.elementPositionY = DataFrame.elementPositionY
-
1should mention: in this example element is anchored at bottom left.– CSBCommented Sep 20, 2017 at 15:03
-
Thanks CSB, Yes for my case I need the data frame to be anchored in the middle, so im just in the process of customizing your Page Origin extents formula, i'll post up the example once I get there. Otherwise looking very promising in initial testing. Also, there is reference to the "unobscured_lyr", assuming this is referenced outside the script as the layer to be avoided?– SlevyCommented Sep 21, 2017 at 1:52
-
correct, the "unobscured_lyr" is the one we're trying not to cover. of course, you could make it work with multiple layers, too.– CSBCommented Sep 21, 2017 at 13:59