11

I have points representing sample locations. Often, multiple samples will be taken in the same location: multiple points with the same location but different sample IDs and other attributes. I'd like to label all points which are co-located with a single label, with stacked text listing all the sample IDs of all the points in that spot.

Is this possible in ArcGIS using either the regular labeling engine or Maplex? I know I could work around this by creating a new layer with all the sample IDs for each location in one attribute value but I'd like to avoid creating new data just for labeling.

Basically I want to go from this:

enter image description here

To this (for the topmost point):

enter image description here

Without doing any manual editing of the labels.

  • How many points are in your dataset? – Hornbydd Jul 28 '15 at 23:45
11

One way of doing this is cloning the layer, using definition queries and labelling them separately, using upper-left only label position for the first layer and lower-left for second.

Add THEFIELD type integer to layer and populate it using expression below:

aList=[]
def FirstOrOthers(shp):
 global aList
 key='%s%s' %(round(shp.firstPoint.X,3),round(shp.firstPoint.Y,3))
 if key in aList:
  return 2   
 aList.append(key)
 return 1

Call it by:

FirstOrOthers( !Shape! )

Create a copy of layer in the table of content, apply definition query THEFIELD=1.

Apply definition query THEFIELD=2 for original layer.

Apply different fixed label placement

enter image description here

UPDATE based on comments to original solution:

Add field COORD and populate it using

'%s %s' %(round( !Shape!.firstPoint.X,2),round( !Shape!.firstPoint.Y,2))

Summarise this field using first and last for label. Join this table back to original using COORD field. Select records where firs<>last and concatenate first and last label in a new field using

'%s\n%s' %(!Sum_Output_4.First_MUID!, !Sum_Output_4.Last_MUID!)

Use Count_COORD and THEFIELD to define 2 'different layers' and fields to label them:

enter image description here

Update #2 inspired by @Hornbydd solution:

import arcpy
def FindLabel ([FID],[MUID]):
  f,m=int([FID]),[MUID]
  mxd = arcpy.mapping.MapDocument("CURRENT")
  dFids={}
  dLabels={}
  lyr = arcpy.mapping.ListLayers(mxd,"centres")[0]
  with arcpy.da.SearchCursor(lyr,["FID","SHAPE@","MUID"]) as cursor:
    for row in cursor:
       FD,shp,LABEL=row
       XY='%s %s' %(round(shp.firstPoint.X,2),round( shp.firstPoint.Y,2))
       if f == FD:
         aKey=XY
       try:
          L=dFids[XY]
          L+=[FD]
          dFids[XY]=L
          L=dLabels[XY]
          L=L+'\n'+LABEL
          dLabels[XY]=L
       except:
          dFids[XY]=[FD]
          dLabels[XY]=LABEL
  Labels=dLabels[aKey]
  Fids=dFids[aKey]
  if f == Fids[0]:
    return Labels
  return ""

UPDATE November 2016, hopefully last.

Below expression tested on 2000 duplicates, works like charm:

mxd = arcpy.mapping.MapDocument("CURRENT")
lyr = arcpy.mapping.ListLayers(mxd,"centres")[0]
dFids={}
dLabels={}
fidKeys={}
with arcpy.da.SearchCursor(lyr,["FID","SHAPE@","MUID"]) as cursor:
 for FD,shp,LABEL in cursor:
  XY='%s %s' %(round(shp.firstPoint.X,2),round( shp.firstPoint.Y,2))
  fidKeys[FD]=XY
  if XY in dLabels:
   dLabels[XY]+=('\n'+LABEL)
   dFids[XY]+=[FD]
  else:
   dLabels[XY]=LABEL
   dFids[XY]=[FD]

def FindLabel ([FID]):
  f=int([FID])
  aKey=fidKeys[f]
  Fids=dFids[aKey]
  if f == Fids[0]:
    return dLabels[aKey]
  return "
  • Hey you cracked it! Nice! I knew there was someone whizzy out there! As I expected it's a very iterative process so run on a big dataset and the labels take for ever to draw (well it did on my test data). I've tweaked your code style by expanding a few lines. I find the minimalist, all on one line difficult to follow. – Hornbydd Jul 29 '15 at 10:07
  • 1
    @Hornbydd thanks for edits. This nut was hard to crack due to label (engine?) behaviour. It treats all of the function parameters as strings! This is why first IF didn’t work without f=int([FID]). Regarding speed I would never use it on a set greater than 50 points. It has to be converted to script that populates new field by going through dataset only twice: 1) search cursor to compile both dictionaries, 2) update cursor to read from them NOTE: first 3 lines after try statement are obsolete, after posting solution I realised they can be safely removed. – FelixIP Jul 29 '15 at 20:17
  • FYI I haven't had a chance to get back to this but I plan to give your solution a whirl in the next week or so. There's also another workaround (the one I used in this case) with no Python, I'll post an answer about that too. – Dan C Aug 7 '15 at 22:00
  • Came across this page on geonet. I liked the clever use of global dictionaries then thought about this Q&A and added a link to it. – Hornbydd Aug 27 '15 at 11:57
  • @Hornbydd yes, it is very smart and detailed like everything from Richard and will make a huge difference to my(ours) solution. One can be further improved by removing few lines which includes the very first one. However based on response time from OP it seems he lost interest, I don't bother either – FelixIP Aug 27 '15 at 20:15
4

Below is a partial solution.

This goes into the Advance label expression. Its not very efficient hence me asking about the number of points in your dataset. So for each row that gets labeled it builds 2 dictionaries d where the key is the XY and the value is the text and d2 which is the objectID and the XY. Using that combination of dictionaries it's able to return a single label which is a concatenation with newline characters, in my example it's concatenating TARGET_FID. "sj" is the layer name in the TOC.

import arcpy
def FindLabel ( [OBJECTID] ):
  ob = str([OBJECTID])
  mxd = arcpy.mapping.MapDocument("CURRENT")
  d ={}
  d2 = {}
  lyr = arcpy.mapping.ListLayers(mxd,"sj")[0]
  with arcpy.da.SearchCursor(lyr,["OID@","SHAPE@XY","TARGET_FID"]) as cursor:
    for row in cursor:
      objID = str(row[0])
      temp = row[1]
      tup = str(temp[0]) + "," + str(temp[1])
      d2[objID] = tup
      txt = str(row[2])
      if tup in d:
        v = d[tup] 
        v = v + "\n" + txt
        d[tup] = v
      else:
        d[tup] = txt  
  temp = d2[ob]
  return d[temp]

Why this is a partial solution is that this is done for every point, I have not been able to think up how you would turn off all the other stacked points. It's because of this I think the ultimate solution is some python that builds a new layer of single points with a single label built from the stack of points.

Below is the output of 3 stacked points as you can see the label is created for each point as they all exist at the same location.

Example

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.