Пример #1
0
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)
Пример #2
0
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
Пример #3
0
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