def CombineMasks(mask_full,SAVEPATH,binnx,binny): # import matplotlib.patches as patches y_arr_f=np.linspace(0,2*ypixels/binny+ygap,2*ypixels/binny+ygap) x_arr_f=np.linspace(0,4*xpixels/binnx+3*xgap,4*xpixels/binnx+3*xgap) X,Y=np.meshgrid(x_arr_f,y_arr_f) fig0,ax0=plt.subplots(1,figsize=(8,8)) cs=ax0.contourf(X,Y,mask_full[0,:,:],cmap=plt.cm.Greys_r) fig0.colorbar(cs,cmap=plt.cm.Greys_r) cs=ax0.contour(X,Y,mask_full[0,:,:],levels=[0.99],color='red',linewidth=2.0) ax0.set_ylim(2*ypixels/binny+ygap,0) #### chip edges.... plt.axhline(y=ypixels/binny,color='yellow') plt.axhline(y=ypixels/binny+ygap,color='yellow') plt.axvline(x=xpixels/binnx,color='yellow') plt.axvline(x=xpixels/binnx+xgap,color='yellow') plt.axvline(x=2*xpixels/binnx+xgap,color='yellow') plt.axvline(x=2*xpixels/binnx+2*xgap,color='yellow') plt.axvline(x=3*xpixels/binnx+2*xgap,color='yellow') plt.axvline(x=3*xpixels/binnx+3*xgap,color='yellow') #### paths=np.load(SAVEPATH+'Masks.npz')['boxes']#cs.collections[0].get_paths() for i in range(0,len(paths)): p0=paths[i] #print p0 bbox=Bbox(np.array([[p0[0],p0[1]],[p0[2],p0[3]]])) #print bbox #print bbox.get_points if np.abs((bbox.get_points()[0,0])-(bbox.get_points()[1,0]))> 190.: ax0.add_patch(patches.Rectangle((p0[0],p0[1]),p0[2]-p0[0],p0[3]-p0[1], facecolor='none', ec='green', linewidth=2, zorder=50)) ax0.set_title('Un-Combined Masks, Full Frame') plt.show(block=False) ##merging masks from split chips boxes=np.array([]) skip_arr=np.array([]) for i in range(0,len(paths)): if i in skip_arr: continue #print '----->', len(boxes)/4. p0=paths[i] bbbox=Bbox(np.array([[p0[0],p0[1]],[p0[2],p0[3]]])) #bbbox=p0.get_extents() #print i, bbox x0,y0,x1,y1=p0[0],p0[1],p0[2],p0[3] #test_point=x0+100 #if np.abs(y1-ypixels)<20: # test_point=[x0+100,y1+2*ygap] #print test_point for j in range(0,len(paths)): if j==i and j<len(paths)-1: j+=1 if j==i and j==len(paths): continue p1=paths[j] x01,y01,x11,y11=p1[0],p1[1],p1[2],p1[3] #if p1.contains_point(test_point): #if test_point[0]>x01 and test_point[0]<x11: if (x0>x01 and x0<x11) or (x1>x01 and x1<x11): if np.abs(y1-y01)<2*ygap: #print i,j skip_arr=np.append(skip_arr,j) #bbox1=p1.get_extents() #x01,y01,x11,y11=bbox1.get_points()[0,0],bbox1.get_points()[0,1],bbox1.get_points()[1,0],bbox1.get_points()[1,1] x0n=np.nanmin([x0,x01]) #print 'Bottom',y01,y11, 'TOP',y0,y1 #print x0,x01,x0n y0n=y0 x1n=np.nanmax([x1,x11]) #print x1,x11,x1n #print '----' y1n=y11 bbbox=Bbox(np.array([[x0n,y0n],[x1n,y1n]])) #elif not p1.contains_point(test_point): # bbox_new=bbbox #else: # bbox_new=bbbox boxes=np.append(boxes,bbbox) mask_edges=np.reshape(boxes,(len(boxes)/4,4))#np.empty([4,len(boxes/4.)]) #p=0 #for b in range(0,len(boxes)): # x=b%4 # mask_edges[x,int(p)]=boxes[b] # p+=(1./4.) ## plotting newly merged boxes fig1,ax1=plt.subplots(1,figsize=(8,8)) cs=ax1.contourf(X,Y,mask_full[0,:,:],cmap=plt.cm.Greys_r) fig1.colorbar(cs,cmap=plt.cm.Greys_r) cs=ax1.contour(X,Y,mask_full[0,:,:],levels=[0.99],color='red',linewidth=2.0) ax1.set_ylim(2*ypixels/binny+ygap,0) #### chip edges.... plt.axhline(y=ypixels/binny,color='yellow') plt.axhline(y=ypixels/binny+ygap,color='yellow') plt.axvline(x=xpixels/binnx,color='yellow') plt.axvline(x=xpixels/binnx+xgap,color='yellow') plt.axvline(x=2*xpixels/binnx+xgap,color='yellow') plt.axvline(x=2*xpixels/binnx+2*xgap,color='yellow') plt.axvline(x=3*xpixels/binnx+2*xgap,color='yellow') plt.axvline(x=3*xpixels/binnx+3*xgap,color='yellow') #### #paths=cs.collections[0].get_paths() for i in range(0,len(boxes)/4): x0,y0,x1,y1=mask_edges[i,0],mask_edges[i,1],mask_edges[i,2],mask_edges[i,3] ax1.add_patch(patches.Rectangle((x0,y0),np.abs(x0-x1),np.abs(y0-y1), facecolor='none', ec='cyan', linewidth=2, zorder=50)) ax1.annotate(i,xy=(x0+100/binnx,y1-500/binny),ha='center',va='center',fontsize=8,color='red',zorder=51) ax1.set_title('Combined Masks, Full Frame') plt.show(block=False) np.savez_compressed(SAVEPATH+'CombinedMasks.npz',mask_edges=mask_edges,boxes=boxes) return(mask_edges)
def apply_labels(labels=None, colors=None, ax=None, magnet=(0.5, 0.75), magnetcoords="axes fraction", padding=None, paddingcoords="offset points", choices="00:02:20:22", n_candidates=32, ignore_filling=False, spacing='linear', _debug=False, **kwargs): """Apply labels directly to series in liu of a legend The `choices` offsets are as follows:: --------------------- | 02 | 12 | 22 | |-------------------| | 01 | XX | 21 | |-------------------| | 00 | 10 | 20 | --------------------- Args: labels (sequence): Optional sequence of labels to override the labels already in the data series colors (str, sequence): color as hex string, list of hex strings to color each label, or an Nx4 ndarray of rgba values for N labels ax (matplotlib.axis): axis; defaults to `plt.gca()` magnet (tuple): prefer positions that are closer to the magnet magnetcoords (str): 'offset pixels', 'offset points' or 'axes fraction' padding (tuple): padding for text in the (x, y) directions paddingcoords (str): 'offset pixels', 'offset points' or 'axes fraction' choices (str): colon separated list of possible label positions relative to the data values. The positions are summarized above. alpha (float): alpha channel (opacity) of label text. Defaults to 1.0 to make text visible. Set to `None` to use the underlying alpha from the handle's color. n_candidates (int): number of potential label locations to consider for each data series. ignore_filling (bool): if True, then assume it's ok to place labels inside paths that are filled with color spacing (str): one of 'linear' or 'random' to specify how far apart candidate locations are spaced along path segments _debug (bool): Mark up all possible label locations **kwargs: passed to plt.annotate Returns: List: annotation objects """ if not ax: ax = plt.gca() if isinstance(colors, (list, tuple)): pass spacing = spacing.strip().lower() if spacing not in ('linear', 'random'): raise ValueError("Spacing '{0}' not understood".format(spacing)) rand_state = np.random.get_state() if spacing == 'random' else None if rand_state is not None: # save the RNG state to restore it later so that plotting functions # don't change the results of scripts that use random numbers np.random.seed(1) _xl, _xh = ax.get_xlim() _yl, _yh = ax.get_ylim() axbb0 = np.array([_xl, _yl]).reshape(1, 2) axbb1 = np.array([_xh, _yh]).reshape(1, 2) # choices:: "01:02:22" -> [(0, 1), (0, 2), (2, 2)] choices = [(int(c[0]), int(c[1])) for c in choices.split(':')] _size = kwargs.get('fontsize', kwargs.get('size', None)) _fontproperties = kwargs.get('fontproperties', None) font_size_pts = text_size_points(size=_size, fontproperties=_fontproperties) # set the default padding equal to the font size if paddingcoords == 'offset pixels': default_padding = font_size_pts * 72 / ax.figure.dpi elif paddingcoords == 'offset points': default_padding = font_size_pts elif paddingcoords == 'axes fraction': default_padding = 0.05 else: raise ValueError("Bad padding coords '{0}'".format(paddingcoords)) # print("fontsize pt:", font_size_pts, # "fontsize px:", xy_as_pixels([font_size_pts, font_size_pts], # 'offset points')[0]) if not isinstance(padding, (list, tuple)): padding = [padding, padding] padding = [default_padding if pd is None else pd for pd in padding] # print("padding::", paddingcoords, padding) magnet_px = xy_as_pixels(magnet, magnetcoords, ax=ax) padding_px = xy_as_pixels(padding, paddingcoords, ax=ax) # print("padding px::", padding_px) annotations = [] cand_map = {} for choice in choices: cand_map[choice] = np.zeros([n_candidates, 2, 2], dtype='f') # these paths are all the paths we can get our hands on so that the text # doesn't overlap them. bboxes around labels are added as we go paths_px = [] # here is a list of bounding boxes around the text boxes as we add them bbox_paths_px = [] is_filled = [] ## how many vertices to avoid ? # artist # collection # image # line # patch # table # container for line in ax.lines: paths_px += [ax.transData.transform_path(line.get_path())] is_filled += [False] for collection in ax.collections: for pth in collection.get_paths(): paths_px += [ax.transData.transform_path(pth)] is_filled += [collection.get_fill()] if ignore_filling: is_filled = [False] * len(is_filled) hands, hand_labels = ax.get_legend_handles_labels() colors = _cycle_colors(colors, len(hands)) # >>> debug >>> if _debug: import viscid from matplotlib import patches as mpatches from viscid.plot import vpyplot as vlt _fig_width = int(ax.figure.bbox.width) _fig_height = int(ax.figure.bbox.height) fig_fld = viscid.zeros((_fig_width, _fig_height), dtype='f', center='node') _X, _Y = fig_fld.get_crds(shaped=True) _axXL, _axYL, _axXH, _axYH = ax.bbox.extents _mask = np.bitwise_and(np.bitwise_and(_X >= _axXL, _X <= _axXH), np.bitwise_and(_Y >= _axYL, _Y <= _axYH)) fig_fld.data[_mask] = 1.0 dfig, dax = plt.subplots(1, 1, figsize=ax.figure.get_size_inches()) vlt.plot(fig_fld, ax=dax, cmap='ocean', colorbar=None) for _, path in enumerate(paths_px): dax.plot(path.vertices[:, 0], path.vertices[:, 1]) dfig.subplots_adjust(bottom=0.0, left=0.0, top=1.0, right=1.0) else: dfig, dax = None, None # <<< debug <<< for i, hand, label_i in zip(count(), hands, hand_labels): if labels and i < len(labels): label = labels[i] else: label = label_i # divine color of label if colors[i]: color = colors[i] else: try: color = hand.get_color() except AttributeError: color = hand.get_facecolor()[0] # get path vertices to determine candidate label positions try: verts = hand.get_path().vertices except AttributeError: verts = [p.vertices for p in hand.get_paths()] verts = np.concatenate(verts, axis=0) segl_dat = verts[:-1, :] segh_dat = verts[1:, :] # take out path segments that have one vertex outside the view _seg_mask = np.all( np.bitwise_and(segl_dat >= axbb0, segl_dat <= axbb1) & np.bitwise_and(segh_dat >= axbb0, segh_dat <= axbb1), axis=1) segl_dat = segl_dat[_seg_mask, :] segh_dat = segh_dat[_seg_mask, :] if np.prod(segl_dat.shape) == 0: print("no full segments are visible, skipping path", i, hand) continue segl_px = ax.transData.transform(segl_dat) segh_px = ax.transData.transform(segh_dat) seglen_px = np.linalg.norm(segh_px - segl_px, axis=1) # take out path segments that are 0 pixels in length _non0_seg_mask = seglen_px > 0 segl_dat = segl_dat[_non0_seg_mask, :] segh_dat = segh_dat[_non0_seg_mask, :] segl_px = segl_px[_non0_seg_mask, :] segh_px = segh_px[_non0_seg_mask, :] seglen_px = seglen_px[_non0_seg_mask] if np.prod(segl_dat.shape) == 0: print("no non-0 segments are visible, skipping path", i, hand) continue # i deeply appologize for how convoluted this got, but the punchline # is that each line segment gets candidates proportinal to their # length in pixels on the figure s_src = np.concatenate([[0], np.cumsum(seglen_px)]) if rand_state is not None: s_dest = s_src[-1] * np.sort(np.random.rand(n_candidates)) else: s_dest = np.linspace(0, s_src[-1], n_candidates) _diff = s_dest.reshape(1, -1) - s_src.reshape(-1, 1) iseg = np.argmin(np.ma.masked_where(_diff <= 0, _diff), axis=0) frac = (s_dest - s_src[iseg]) / seglen_px[iseg] root_dat = (segl_dat[iseg] + frac.reshape(-1, 1) * (segh_dat[iseg] - segl_dat[iseg])) root_px = ax.transData.transform(root_dat) # estimate the width and height of the label's text txt_size = np.array( estimate_text_size_px(label, fig=ax.figure, size=font_size_pts)) txt_size = txt_size.reshape([1, 2]) # this initial offset is needed to shift the center of the label # to the data point offset0 = -txt_size / 2 # now we can shift the label away from the data point by an amount # equal to half the text width/height + the padding offset1 = padding_px + txt_size / 2 for key, abs_px_arr in cand_map.items(): ioff = np.array(key, dtype='i').reshape(1, 2) - 1 total_offset = offset0 + ioff * offset1 # approx lower left corner of the text box in absolute pixels abs_px_arr[:, :, 0] = root_px + total_offset # approx upper right corner of the text box in absolute pixels abs_px_arr[:, :, 1] = abs_px_arr[:, :, 0] + txt_size # candidates_abs_px[i] has root @ root_px[i % n_candidates] candidates_abs_px = np.concatenate([cand_map[c] for c in choices], axis=0) # find how many other things each candidate overlaps n_overlaps = np.zeros_like(candidates_abs_px[:, 0, 0]) for k, candidate in enumerate(candidates_abs_px): cand_bbox = Bbox(candidate.T) # penalty for each time a box overlaps a path that's already # on the plot for ipth, path in enumerate(paths_px): if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]): n_overlaps[k] += 1 # slightly larger penalty if we intersect a text box that we # just added to the plot for ipth, path in enumerate(bbox_paths_px): if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]): n_overlaps[k] += 5 # big penalty if the candidate is out of the current view if not (ax.bbox.contains(*cand_bbox.min) and ax.bbox.contains(*cand_bbox.max)): n_overlaps[k] += 100 # sort candidates by distance between center of text box and magnet magnet_dist = np.linalg.norm(np.mean(candidates_abs_px, axis=-1) - magnet_px, axis=1) isorted = np.argsort(magnet_dist) magnet_dist = np.array(magnet_dist[isorted]) candidates_abs_px = np.array(candidates_abs_px[isorted, :, :]) n_overlaps = np.array(n_overlaps[isorted]) root_dat = np.array(root_dat[isorted % n_candidates, :]) root_px = np.array(root_px[isorted % n_candidates, :]) # sort candidates so the ones with the fewest overlaps are first # but do it with a stable algorithm so among the best candidates, # choose the one closest to the magnet sargs = np.argsort(n_overlaps, kind='mergesort') # >>> debug >>> if dax is not None: for _candidate, n_overlap in zip(candidates_abs_px, n_overlaps): _cand_bbox = Bbox(_candidate.T) _x0 = _cand_bbox.get_points()[0] _bbox_center = np.mean(_candidate, axis=-1) _ray_x = [_bbox_center[0], magnet_px[0]] _ray_y = [_bbox_center[1], magnet_px[1]] dax.plot(_ray_x, _ray_y, '-', alpha=0.3, color='grey') _rect = mpatches.Rectangle(_x0, _cand_bbox.width, _cand_bbox.height, fill=False) dax.add_patch(_rect) plt.text(_x0[0], _x0[1], label, color='gray') plt.text(_x0[0], _x0[1], '{0}'.format(n_overlap)) # <<< debug <<< # pick winning candidate and add its bounding box to this list of # paths to avoid winner_abs_px = candidates_abs_px[sargs[0], :, :] xy_root_px = root_px[sargs[0], :] xy_root_dat = np.array(root_dat[sargs[0], :]) xy_txt_offset = np.array(winner_abs_px[:, 0] - xy_root_px) corners = Bbox(winner_abs_px.T).corners()[(0, 1, 3, 2), :] bbox_paths_px += [Path(corners)] # a = plt.annotate(label, xy=xy_root_dat, xycoords='data', # xytext=xy_txt_offset, textcoords="offset pixels", # color=color, **kwargs) a = ax.annotate(label, xy=xy_root_dat, xycoords='data', xytext=xy_txt_offset, textcoords="offset pixels", color=color, **kwargs) annotations.append(a) if rand_state is not None: np.random.set_state(rand_state) return annotations
def apply_labels(labels=None, colors=None, ax=None, magnet=(0.5, 0.75), magnetcoords="axes fraction", padding=None, paddingcoords="offset points", choices="00:02:20:22", n_candidates=32, ignore_filling=False, spacing='linear', _debug=False, **kwargs): """Apply labels directly to series in liu of a legend The `choices` offsets are as follows:: --------------------- | 02 | 12 | 22 | |-------------------| | 01 | XX | 21 | |-------------------| | 00 | 10 | 20 | --------------------- Args: labels (sequence): Optional sequence of labels to override the labels already in the data series colors (str, sequence): color as hex string, list of hex strings to color each label, or an Nx4 ndarray of rgba values for N labels ax (matplotlib.axis): axis; defaults to `plt.gca()` magnet (tuple): prefer positions that are closer to the magnet magnetcoords (str): 'offset pixels', 'offset points' or 'axes fraction' padding (tuple): padding for text in the (x, y) directions paddingcoords (str): 'offset pixels', 'offset points' or 'axes fraction' choices (str): colon separated list of possible label positions relative to the data values. The positions are summarized above. alpha (float): alpha channel (opacity) of label text. Defaults to 1.0 to make text visible. Set to `None` to use the underlying alpha from the handle's color. n_candidates (int): number of potential label locations to consider for each data series. ignore_filling (bool): if True, then assume it's ok to place labels inside paths that are filled with color spacing (str): one of 'linear' or 'random' to specify how far apart candidate locations are spaced along path segments _debug (bool): Mark up all possible label locations **kwargs: passed to plt.annotate Returns: List: annotation objects """ if not ax: ax = plt.gca() if isinstance(colors, (list, tuple)): pass spacing = spacing.strip().lower() if spacing not in ('linear', 'random'): raise ValueError("Spacing '{0}' not understood".format(spacing)) rand_state = np.random.get_state() if spacing == 'random' else None if rand_state is not None: # save the RNG state to restore it later so that plotting functions # don't change the results of scripts that use random numbers np.random.seed(1) _xl, _xh = ax.get_xlim() _yl, _yh = ax.get_ylim() axbb0 = np.array([_xl, _yl]).reshape(1, 2) axbb1 = np.array([_xh, _yh]).reshape(1, 2) # choices:: "01:02:22" -> [(0, 1), (0, 2), (2, 2)] choices = [(int(c[0]), int(c[1])) for c in choices.split(':')] _size = kwargs.get('fontsize', kwargs.get('size', None)) _fontproperties = kwargs.get('fontproperties', None) font_size_pts = text_size_points(size=_size, fontproperties=_fontproperties) # set the default padding equal to the font size if paddingcoords == 'offset pixels': default_padding = font_size_pts * 72 / ax.figure.dpi elif paddingcoords == 'offset points': default_padding = font_size_pts elif paddingcoords == 'axes fraction': default_padding = 0.05 else: raise ValueError("Bad padding coords '{0}'".format(paddingcoords)) # print("fontsize pt:", font_size_pts, # "fontsize px:", xy_as_pixels([font_size_pts, font_size_pts], # 'offset points')[0]) if not isinstance(padding, (list, tuple)): padding = [padding, padding] padding = [default_padding if pd is None else pd for pd in padding] # print("padding::", paddingcoords, padding) magnet_px = xy_as_pixels(magnet, magnetcoords, ax=ax) padding_px = xy_as_pixels(padding, paddingcoords, ax=ax) # print("padding px::", padding_px) annotations = [] cand_map = {} for choice in choices: cand_map[choice] = np.zeros([n_candidates, 2, 2], dtype='f') # these paths are all the paths we can get our hands on so that the text # doesn't overlap them. bboxes around labels are added as we go paths_px = [] # here is a list of bounding boxes around the text boxes as we add them bbox_paths_px = [] is_filled = [] ## how many vertices to avoid ? # artist # collection # image # line # patch # table # container for line in ax.lines: paths_px += [ax.transData.transform_path(line.get_path())] is_filled += [False] for collection in ax.collections: for pth in collection.get_paths(): paths_px += [ax.transData.transform_path(pth)] is_filled += [collection.get_fill()] if ignore_filling: is_filled = [False] * len(is_filled) hands, hand_labels = ax.get_legend_handles_labels() colors = _cycle_colors(colors, len(hands)) # >>> debug >>> if _debug: import viscid from matplotlib import patches as mpatches from viscid.plot import vpyplot as vlt _fig_width = int(ax.figure.bbox.width) _fig_height = int(ax.figure.bbox.height) fig_fld = viscid.zeros((_fig_width, _fig_height), dtype='f', center='node') _X, _Y = fig_fld.get_crds(shaped=True) _axXL, _axYL, _axXH, _axYH = ax.bbox.extents _mask = np.bitwise_and(np.bitwise_and(_X >= _axXL, _X <= _axXH), np.bitwise_and(_Y >= _axYL, _Y <= _axYH)) fig_fld.data[_mask] = 1.0 dfig, dax = plt.subplots(1, 1, figsize=ax.figure.get_size_inches()) vlt.plot(fig_fld, ax=dax, cmap='ocean', colorbar=None) for _, path in enumerate(paths_px): dax.plot(path.vertices[:, 0], path.vertices[:, 1]) dfig.subplots_adjust(bottom=0.0, left=0.0, top=1.0, right=1.0) else: dfig, dax = None, None # <<< debug <<< for i, hand, label_i in zip(count(), hands, hand_labels): if labels and i < len(labels): label = labels[i] else: label = label_i # divine color of label if colors[i]: color = colors[i] else: try: color = hand.get_color() except AttributeError: color = hand.get_facecolor()[0] # get path vertices to determine candidate label positions try: verts = hand.get_path().vertices except AttributeError: verts = [p.vertices for p in hand.get_paths()] verts = np.concatenate(verts, axis=0) segl_dat = verts[:-1, :] segh_dat = verts[1:, :] # take out path segments that have one vertex outside the view _seg_mask = np.all(np.bitwise_and(segl_dat >= axbb0, segl_dat <= axbb1) & np.bitwise_and(segh_dat >= axbb0, segh_dat <= axbb1), axis=1) segl_dat = segl_dat[_seg_mask, :] segh_dat = segh_dat[_seg_mask, :] if np.prod(segl_dat.shape) == 0: print("no full segments are visible, skipping path", i, hand) continue segl_px = ax.transData.transform(segl_dat) segh_px = ax.transData.transform(segh_dat) seglen_px = np.linalg.norm(segh_px - segl_px, axis=1) # take out path segments that are 0 pixels in length _non0_seg_mask = seglen_px > 0 segl_dat = segl_dat[_non0_seg_mask, :] segh_dat = segh_dat[_non0_seg_mask, :] segl_px = segl_px[_non0_seg_mask, :] segh_px = segh_px[_non0_seg_mask, :] seglen_px = seglen_px[_non0_seg_mask] if np.prod(segl_dat.shape) == 0: print("no non-0 segments are visible, skipping path", i, hand) continue # i deeply appologize for how convoluted this got, but the punchline # is that each line segment gets candidates proportinal to their # length in pixels on the figure s_src = np.concatenate([[0], np.cumsum(seglen_px)]) if rand_state is not None: s_dest = s_src[-1] * np.sort(np.random.rand(n_candidates)) else: s_dest = np.linspace(0, s_src[-1], n_candidates) _diff = s_dest.reshape(1, -1) - s_src.reshape(-1, 1) iseg = np.argmin(np.ma.masked_where(_diff <= 0, _diff), axis=0) frac = (s_dest - s_src[iseg]) / seglen_px[iseg] root_dat = (segl_dat[iseg] + frac.reshape(-1, 1) * (segh_dat[iseg] - segl_dat[iseg])) root_px = ax.transData.transform(root_dat) # estimate the width and height of the label's text txt_size = np.array(estimate_text_size_px(label, fig=ax.figure, size=font_size_pts)) txt_size = txt_size.reshape([1, 2]) # this initial offset is needed to shift the center of the label # to the data point offset0 = -txt_size / 2 # now we can shift the label away from the data point by an amount # equal to half the text width/height + the padding offset1 = padding_px + txt_size / 2 for key, abs_px_arr in cand_map.items(): ioff = np.array(key, dtype='i').reshape(1, 2) - 1 total_offset = offset0 + ioff * offset1 # approx lower left corner of the text box in absolute pixels abs_px_arr[:, :, 0] = root_px + total_offset # approx upper right corner of the text box in absolute pixels abs_px_arr[:, :, 1] = abs_px_arr[:, :, 0] + txt_size # candidates_abs_px[i] has root @ root_px[i % n_candidates] candidates_abs_px = np.concatenate([cand_map[c] for c in choices], axis=0) # find how many other things each candidate overlaps n_overlaps = np.zeros_like(candidates_abs_px[:, 0, 0]) for k, candidate in enumerate(candidates_abs_px): cand_bbox = Bbox(candidate.T) # penalty for each time a box overlaps a path that's already # on the plot for ipth, path in enumerate(paths_px): if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]): n_overlaps[k] += 1 # slightly larger penalty if we intersect a text box that we # just added to the plot for ipth, path in enumerate(bbox_paths_px): if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]): n_overlaps[k] += 5 # big penalty if the candidate is out of the current view if not (ax.bbox.contains(*cand_bbox.min) and ax.bbox.contains(*cand_bbox.max)): n_overlaps[k] += 100 # sort candidates by distance between center of text box and magnet magnet_dist = np.linalg.norm(np.mean(candidates_abs_px, axis=-1) - magnet_px, axis=1) isorted = np.argsort(magnet_dist) magnet_dist = np.array(magnet_dist[isorted]) candidates_abs_px = np.array(candidates_abs_px[isorted, :, :]) n_overlaps = np.array(n_overlaps[isorted]) root_dat = np.array(root_dat[isorted % n_candidates, :]) root_px = np.array(root_px[isorted % n_candidates, :]) # sort candidates so the ones with the fewest overlaps are first # but do it with a stable algorithm so among the best candidates, # choose the one closest to the magnet sargs = np.argsort(n_overlaps, kind='mergesort') # >>> debug >>> if dax is not None: for _candidate, n_overlap in zip(candidates_abs_px, n_overlaps): _cand_bbox = Bbox(_candidate.T) _x0 = _cand_bbox.get_points()[0] _bbox_center = np.mean(_candidate, axis=-1) _ray_x = [_bbox_center[0], magnet_px[0]] _ray_y = [_bbox_center[1], magnet_px[1]] dax.plot(_ray_x, _ray_y, '-', alpha=0.3, color='grey') _rect = mpatches.Rectangle(_x0, _cand_bbox.width, _cand_bbox.height, fill=False) dax.add_patch(_rect) plt.text(_x0[0], _x0[1], label, color='gray') plt.text(_x0[0], _x0[1], '{0}'.format(n_overlap)) # <<< debug <<< # pick winning candidate and add its bounding box to this list of # paths to avoid winner_abs_px = candidates_abs_px[sargs[0], :, :] xy_root_px = root_px[sargs[0], :] xy_root_dat = np.array(root_dat[sargs[0], :]) xy_txt_offset = np.array(winner_abs_px[:, 0] - xy_root_px) corners = Bbox(winner_abs_px.T).corners()[(0, 1, 3, 2), :] bbox_paths_px += [Path(corners)] # a = plt.annotate(label, xy=xy_root_dat, xycoords='data', # xytext=xy_txt_offset, textcoords="offset pixels", # color=color, **kwargs) a = ax.annotate(label, xy=xy_root_dat, xycoords='data', xytext=xy_txt_offset, textcoords="offset pixels", color=color, **kwargs) annotations.append(a) if rand_state is not None: np.random.set_state(rand_state) return annotations