8

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.

enter image description here

asked Sep 11, 2017 at 6:45
2
  • 1
    If 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. Commented Sep 11, 2017 at 12:20
  • Yes im currently using data driven pages, thanks for the link Johns Commented Sep 13, 2017 at 0:54

3 Answers 3

7
+50

Inputs: enter image description here Script:

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()

OUTPUT: enter image description here

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.

answered Sep 16, 2017 at 3:44
5
  • 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? Commented Sep 18, 2017 at 6:09
  • 1
    Anchor points are lower left for both legend and data frame. How did I forget this? Commented 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? Commented Sep 18, 2017 at 6:39
  • 1
    Compute xmin and xmax through width and position x. Similar with y axis. Not sure why do you need it... Commented Sep 18, 2017 at 7:01
  • Part of another workflow, thanks Felix, great step forward here Commented Sep 18, 2017 at 7:14
3

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.

answered Sep 11, 2017 at 7:08
3
  • 1
    The hardest part is findinga gap big enough to fit legend box. Commented 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. 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! Commented Sep 13, 2017 at 0:53
2

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
answered Sep 19, 2017 at 16:31
3
  • 1
    should mention: in this example element is anchored at bottom left. Commented 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? Commented 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. Commented Sep 21, 2017 at 13:59

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.